From 08ac8716f1c12dd9c989726992e8152284cda55d Mon Sep 17 00:00:00 2001 From: Mathew Date: Sun, 30 Aug 2026 17:11:19 -0500 Subject: [PATCH] docs: architecture review, ADR acceptance, cross-GDD reconciliation, test scaffolding MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Checkpoint of the pre-production design + architecture pass. First commit of the full game design corpus for "What the Gods Left Behind" plus the architecture decision layer and test infrastructure. Architecture: - /architecture-review 2026-08-21 (CONCERNS): review report, traceability index, tr-registry.yaml populated with 399 stable TR-IDs. - ADR-0001 (instance combat-stat modifier layer) and ADR-0002 (TimeControl) moved Proposed->Accepted with engine-audit cleanups; architecture.yaml stances wired. - ADR-0003 (Save/Persistence) and ADR-0004 (Era lifecycle/EraManager) added by the parallel workstream — still Proposed, reconciliation pending. Cross-GDD reconciliation (closes cross-review 2026-08-18 blockers): - F2-B1 effective_attack_damage (ADR-0001), F2-B4/S2 offering_time_scale (ADR-0002), F2-B3/B7 Reliquias reciprocal wiring, F2-B6 systems-index dependency map, F2-W3 Furia exception in Combate Regla 5. - F2-B5 band cap: max_band_size authored in EraDefinition (Tropas Formula E), two independent faucets; entities.yaml constant registered. - S1 Regla 10 origin_era_id pool filter made implementable and consistent. - S7/OQ-15 (Furia freeze scope + sacrifice loophole) verified resolved. - Only S4/persistence remains, in flight as ADR-0003. Test infrastructure (/test-setup, Godot 4.6 / GdUnit4): - tests/ layout, gdunit4_runner.gd, smoke critical-paths, self-contained example unit test; .github/workflows/tests.yml CI. Framework corrected GUT->GdUnit4. Also: CLAUDE.md engine (Godot 4.6/GDScript), gitignore session-state/, agent-memory. Co-Authored-By: Claude Opus 4.8 --- .claude/agent-memory/art-director/MEMORY.md | 1 + .../art-director/reference_art-bible-map.md | 18 + .claude/agent-memory/game-designer/MEMORY.md | 1 + .../project_input-silent-discard-pattern.md | 14 + .../performance-analyst/MEMORY.md | 1 + .../project_input_gdd_perf_gaps.md | 17 + .claude/agent-memory/qa-lead/MEMORY.md | 5 + .../project_heroes-system-ac-status.md | 83 ++ .../qa-lead/project_input-system-ac-status.md | 68 ++ .../agent-memory/qa-lead/project_overview.md | 16 + .../project_save-persistence-status.md | 21 + .../qa-lead/project_troop-system-ac-status.md | 50 ++ .../agent-memory/systems-designer/MEMORY.md | 1 + .../project_wtgleft_context.md | 32 + .claude/docs/technical-preferences.md | 71 +- .github/workflows/tests.yml | 37 + .gitignore | 1 + CLAUDE.md | 8 +- design/art/art-bible.md | 484 +++++++++++ design/gdd/combate-dano.md | 484 +++++++++++ design/gdd/control-y-seleccion-de-unidades.md | 312 +++++++ design/gdd/datos-de-era-civilizacion.md | 216 +++++ design/gdd/encuentro-con-kaiju.md | 799 ++++++++++++++++++ design/gdd/forja-de-legado.md | 349 ++++++++ design/gdd/game-concept.md | 271 ++++++ design/gdd/gdd-cross-review-2026-08-18.md | 164 ++++ design/gdd/guardado-persistencia.md | 271 ++++++ design/gdd/input.md | 299 +++++++ design/gdd/permadeath.md | 390 +++++++++ design/gdd/reliquias-bendiciones.md | 578 +++++++++++++ design/gdd/reviews/combate-dano-review-log.md | 42 + .../reviews/encuentro-con-kaiju-review-log.md | 85 ++ .../gdd/reviews/forja-de-legado-review-log.md | 38 + design/gdd/reviews/input-review-log.md | 26 + design/gdd/reviews/permadeath-review-log.md | 264 ++++++ .../reliquias-bendiciones-review-log.md | 32 + .../reviews/sistema-de-heroes-review-log.md | 101 +++ ...izador-de-preparacion-ritual-review-log.md | 38 + design/gdd/reviews/ui-hud-review-log.md | 24 + design/gdd/sistema-de-heroes.md | 515 +++++++++++ design/gdd/sistema-de-tropas.md | 309 +++++++ design/gdd/systems-index.md | 183 ++++ .../gdd/temporizador-de-preparacion-ritual.md | 352 ++++++++ design/gdd/ui-hud.md | 335 ++++++++ design/registry/entities.yaml | 441 +++++++++- .../adr-0001-combat-stat-modifier-layer.md | 198 +++++ .../adr-0002-time-control-service.md | 220 +++++ ...adr-0003-save-persistence-serialization.md | 262 ++++++ .../adr-0004-era-lifecycle-init-ordering.md | 264 ++++++ .../architecture-review-2026-08-21.md | 231 +++++ docs/architecture/tr-registry.yaml | 458 +++++++++- docs/architecture/traceability-index.md | 80 ++ docs/consistency-report-2026-08-06.md | 50 ++ docs/consistency-report-2026-08-12.md | 64 ++ docs/consistency-report-2026-08-14.md | 38 + docs/consistency-report-2026-08-16.md | 46 + docs/consistency-report-2026-08-18.md | 51 ++ docs/registry/architecture.yaml | 253 +++++- tests/README.md | 73 ++ tests/evidence/.gitkeep | 2 + tests/gdunit4_runner.gd | 13 + tests/integration/.gdignore_placeholder | 1 + tests/smoke/critical-paths.md | 33 + tests/unit/.gdignore_placeholder | 1 + tests/unit/combate/combat_damage_test.gd | 33 + 65 files changed, 10136 insertions(+), 82 deletions(-) create mode 100644 .claude/agent-memory/art-director/MEMORY.md create mode 100644 .claude/agent-memory/art-director/reference_art-bible-map.md create mode 100644 .claude/agent-memory/game-designer/MEMORY.md create mode 100644 .claude/agent-memory/game-designer/project_input-silent-discard-pattern.md create mode 100644 .claude/agent-memory/performance-analyst/MEMORY.md create mode 100644 .claude/agent-memory/performance-analyst/project_input_gdd_perf_gaps.md create mode 100644 .claude/agent-memory/qa-lead/MEMORY.md create mode 100644 .claude/agent-memory/qa-lead/project_heroes-system-ac-status.md create mode 100644 .claude/agent-memory/qa-lead/project_input-system-ac-status.md create mode 100644 .claude/agent-memory/qa-lead/project_overview.md create mode 100644 .claude/agent-memory/qa-lead/project_save-persistence-status.md create mode 100644 .claude/agent-memory/qa-lead/project_troop-system-ac-status.md create mode 100644 .claude/agent-memory/systems-designer/MEMORY.md create mode 100644 .claude/agent-memory/systems-designer/project_wtgleft_context.md create mode 100644 .github/workflows/tests.yml create mode 100644 design/art/art-bible.md create mode 100644 design/gdd/combate-dano.md create mode 100644 design/gdd/control-y-seleccion-de-unidades.md create mode 100644 design/gdd/datos-de-era-civilizacion.md create mode 100644 design/gdd/encuentro-con-kaiju.md create mode 100644 design/gdd/forja-de-legado.md create mode 100644 design/gdd/game-concept.md create mode 100644 design/gdd/gdd-cross-review-2026-08-18.md create mode 100644 design/gdd/guardado-persistencia.md create mode 100644 design/gdd/input.md create mode 100644 design/gdd/permadeath.md create mode 100644 design/gdd/reliquias-bendiciones.md create mode 100644 design/gdd/reviews/combate-dano-review-log.md create mode 100644 design/gdd/reviews/encuentro-con-kaiju-review-log.md create mode 100644 design/gdd/reviews/forja-de-legado-review-log.md create mode 100644 design/gdd/reviews/input-review-log.md create mode 100644 design/gdd/reviews/permadeath-review-log.md create mode 100644 design/gdd/reviews/reliquias-bendiciones-review-log.md create mode 100644 design/gdd/reviews/sistema-de-heroes-review-log.md create mode 100644 design/gdd/reviews/temporizador-de-preparacion-ritual-review-log.md create mode 100644 design/gdd/reviews/ui-hud-review-log.md create mode 100644 design/gdd/sistema-de-heroes.md create mode 100644 design/gdd/sistema-de-tropas.md create mode 100644 design/gdd/systems-index.md create mode 100644 design/gdd/temporizador-de-preparacion-ritual.md create mode 100644 design/gdd/ui-hud.md create mode 100644 docs/architecture/adr-0001-combat-stat-modifier-layer.md create mode 100644 docs/architecture/adr-0002-time-control-service.md create mode 100644 docs/architecture/adr-0003-save-persistence-serialization.md create mode 100644 docs/architecture/adr-0004-era-lifecycle-init-ordering.md create mode 100644 docs/architecture/architecture-review-2026-08-21.md create mode 100644 docs/architecture/traceability-index.md create mode 100644 docs/consistency-report-2026-08-06.md create mode 100644 docs/consistency-report-2026-08-12.md create mode 100644 docs/consistency-report-2026-08-14.md create mode 100644 docs/consistency-report-2026-08-16.md create mode 100644 docs/consistency-report-2026-08-18.md create mode 100644 tests/README.md create mode 100644 tests/evidence/.gitkeep create mode 100644 tests/gdunit4_runner.gd create mode 100644 tests/integration/.gdignore_placeholder create mode 100644 tests/smoke/critical-paths.md create mode 100644 tests/unit/.gdignore_placeholder create mode 100644 tests/unit/combate/combat_damage_test.gd diff --git a/.claude/agent-memory/art-director/MEMORY.md b/.claude/agent-memory/art-director/MEMORY.md new file mode 100644 index 0000000000..0b9971aadf --- /dev/null +++ b/.claude/agent-memory/art-director/MEMORY.md @@ -0,0 +1 @@ +- [Art Bible Map](reference_art-bible-map.md) — section map + key rules (palette, shape hierarchy, UI frame-tier system) for design/art/art-bible.md diff --git a/.claude/agent-memory/art-director/reference_art-bible-map.md b/.claude/agent-memory/art-director/reference_art-bible-map.md new file mode 100644 index 0000000000..47d1a86870 --- /dev/null +++ b/.claude/agent-memory/art-director/reference_art-bible-map.md @@ -0,0 +1,18 @@ +--- +name: reference-art-bible-map +description: Location and section map of the project's art bible — check before any visual-direction consultation to cite correct section numbers. +metadata: + type: reference +--- + +`design/art/art-bible.md` — "What the Gods Left Behind" (Gótico Pixelado Sagrado), ~485 lines, 9 sections, v1.0 as of 2026-07-23, owned by art-director. + +Section map (verify line numbers still match before citing — file may grow): +1. Visual Identity Statement — Principle A (Legado es Luz, gold reserved for permadeath-derived), Principle B (scale), Principle C (camera holds gaze on death), reference anchors (Blasphemous 2 / Dark Souls / Hollow Knight) +2. Mood & Atmosphere — per game-state (Preparación, Clímax, Muerte/Sacrificio, Salón Conmemorativo, Transición de Era, Victoria) — 2.2 established the "screen-edge stained-glass crack" motif for kaiju attacks, reusable for any kaiju-telegraph HUD work +3. Shape Language — 3.1 archetypal silhouettes (tropa=vertical astilla uniform, héroe=silueta interrumpida via ONE asymmetric element, kaiju=low/wide horizon-breaker, esbirros=kaiju-family angular flattened), 3.3 UI is fully diegetic (no neutral HUD layer, tradeoff noted), 3.4 eye-priority hierarchy (kaiju > hero > troops > environment) +4. Color System — 4.1 the 7-color palette (6 muted + Oro Reliquia `#F4C542`/`#FFE9A8` exclusive to permadeath-derived elements), 4.2 semantic assignments (Sangre Vieja=active cost/health, Acero Violeta-Ceniza=threat/kaiju/dread, Hueso=living player ally, gold=earned/legendary only), 4.4 UI palette (health bar Hueso→Sangre Vieja drain, selection outline = Acero thin contour no fill, ability-ready gold ONLY for relic-derived abilities), 4.5 colorblind-safety table (gold's redundant channels: bloom/pulse + particle VFX + frame-shape change — never hue alone) +7. UI/HUD Visual Direction — 7.1 two-track typography (Tallada display / Clara numeric — numerics MUST use Clara during combat), 7.2 icon tiers (base=flat silhouette, relic-derived=gold leaded-line detail inside silhouette, panel=full stained-glass, status/debuff=flat max-contrast never gold), 7.3 animation feel (glass/wood-metal physicality, no elastic/bounce easing — reads as arcade), 7.4 frame ornamentation tiers 0-3 (0=no frame/cool marker, 1=simple leaded rect+Hueso ready-edge, 2=leaded+corner tracery+gold only when ready, 3=full stained-glass saturated gold, reserved for Memorial/relic panels only — no 5th tier allowed), 7.5 UX mitigations already agreed (threshold-triggered diegetic events for multi-hero health reading in Climax, distinct rare chime for relic-ability-ready vs. base-ability, opt-in high-contrast accessibility toggle deferred to design/ux/accessibility-requirements.md) +5-6, 8-9: character design direction, environment language, asset standards (naming `[category]_[name]_[variant]_[size]`, Aseprite→lossless PNG, Nearest filtering, no mipmaps on pixel sprites), reference direction (Blasphemous 2/Dark Souls/Hollow Knight/Shadow of Colossus/Godzilla/Pacific Rim — each with explicit "extract this, avoid that" pairs, see 9.7-9.8 anti-redundancy table) + +Useful for: any UI/HUD, VFX, or character-visual consultation on this project — the 7-color palette and 7.4 frame-tier system in particular recur across almost every HUD-adjacent design question. diff --git a/.claude/agent-memory/game-designer/MEMORY.md b/.claude/agent-memory/game-designer/MEMORY.md new file mode 100644 index 0000000000..27b8401b43 --- /dev/null +++ b/.claude/agent-memory/game-designer/MEMORY.md @@ -0,0 +1 @@ +- [Input silent-discard pattern](project_input-silent-discard-pattern.md) — input.md discards ambiguous input with zero feedback cue; check sibling GDDs for same gap. diff --git a/.claude/agent-memory/game-designer/project_input-silent-discard-pattern.md b/.claude/agent-memory/game-designer/project_input-silent-discard-pattern.md new file mode 100644 index 0000000000..0e76c67f5b --- /dev/null +++ b/.claude/agent-memory/game-designer/project_input-silent-discard-pattern.md @@ -0,0 +1,14 @@ +--- +name: project_input-silent-discard-pattern +description: Cross-cutting design gap found in input.md — ambiguous/suppressed states resolve by silently discarding player input with zero rejection feedback, undermining Pillar 1's trust promise. Worth re-checking in sibling GDDs. +metadata: + type: project +--- + +During the `/design-review` of `design/gdd/input.md` (2026-08-19), found that "silently discard, no queueing" is used as the default resolution strategy for *every* ambiguous or suppressed input state in the document: `DEATH_HOLD` suppression (all input), mid-action device switch (input on the non-active device), and pause-during-`BLESSING_SELECT` cancellation. None of these paths define a rejection cue (sound, visual flash, haptic pulse) — the player has no way to distinguish "the game intentionally ignored this input" from "the game is lagging/broken." + +**Why this matters**: "What the Gods Left Behind"'s Pillar 1 (Sacrificio con Peso) requires every hero death to read as a legitimate player decision, never a technical accident — input.md's own Player Fantasy section stakes this explicitly ("la entrada del jugador debe ser tan confiable que... el jugador nunca pueda culpar al control"). A discard-with-zero-feedback pattern is exactly the kind of ambiguity that could make a death-adjacent moment feel like a technical accident, especially during `DEATH_HOLD` — the single highest-stakes moment in the whole input system — where a player instinctively mashing buttons in the moment of loss gets total silence back. + +**How to apply**: When reviewing `permadeath.md`, `reliquias-bendiciones.md`, `control-y-seleccion-de-unidades.md`, or `ui-hud.md` (none had GDDs yet as of 2026-08-19 per input.md's provisional notes), check whether they inherit this same silent-discard pattern for their own ambiguous/suppressed states, and whether any of them define the missing rejection-feedback cue as a shared contract. Flag it again if still unaddressed — this is a systemic gap, not a one-off. + +See also [[input-gdd-review-2026-08-19]] if a fuller memory of that review is written later. diff --git a/.claude/agent-memory/performance-analyst/MEMORY.md b/.claude/agent-memory/performance-analyst/MEMORY.md new file mode 100644 index 0000000000..5c4ba9c85b --- /dev/null +++ b/.claude/agent-memory/performance-analyst/MEMORY.md @@ -0,0 +1 @@ +- [Input GDD perf gaps](project_input_gdd_perf_gaps.md) — AC 22 unfalsifiable, no Input budget row, gamepad cursor undesigned/unbudgeted (review 2026-08-19) diff --git a/.claude/agent-memory/performance-analyst/project_input_gdd_perf_gaps.md b/.claude/agent-memory/performance-analyst/project_input_gdd_perf_gaps.md new file mode 100644 index 0000000000..6867b2e31d --- /dev/null +++ b/.claude/agent-memory/performance-analyst/project_input_gdd_perf_gaps.md @@ -0,0 +1,17 @@ +--- +name: project-input-gdd-perf-gaps +description: Performance gaps found in design/gdd/input.md AC 22 (latency gate) and missing Input budget category, from 2026-08-19 review +metadata: + type: project +--- + +Reviewed `design/gdd/input.md` (Sistema de Input, "pendiente de review" as of 2026-08-19) for performance issues. Key open gaps as of that review: + +- **AC 22** ("≤1 frame / ≤16.6ms latency, 0 eventos caídos, medido en 60s") has no stated instrumentation strategy for detecting *dropped* events — you can't measure the absence of a signal without an independent record of physical OS events (timestamp diffing or synthetic input injection). No test harness for this is described anywhere in the doc, and Dependencies explicitly says Input has zero upstream deps. This makes AC 22 currently unfalsifiable as a BLOQUEANTE gate. +- The GDD doesn't specify whether intent-signal emission happens synchronously inside `_input`/`_unhandled_input` or via a deferred call — this determines whether "same frame" is guaranteed by construction or just asserted. +- The project's standard Frame Time Budget report table (Gameplay Logic / Rendering / Physics / AI / Audio) has **no Input row**, despite Input carrying a hard latency AC. Escalated as a gap for `technical-director` to decide: fold into Gameplay Logic, or add a dedicated category. +- `technical-preferences.md` calls for an explicitly-designed gamepad cursor/selection system for console (not a straight remap), but `input.md` doesn't design it at all — so its render/update cost per frame is currently unknown and unbudgeted. Worth checking whether a later revision of input.md (or a new GDD) addresses this before implementation. + +**Why**: input.md is Foundation-tier and gates Pilar 1 (Sacrificio con Peso) — a hero must never die from a dropped/laggy input. These gaps block a clean performance sign-off. + +**How to apply**: If input.md comes back for review/implementation sign-off, check whether the AC 22 instrumentation approach and the Input budget-category question were resolved before treating the BLOQUEANTE gate as satisfied. Check for a gamepad-cursor design (this doc or a new one) before console input work starts. diff --git a/.claude/agent-memory/qa-lead/MEMORY.md b/.claude/agent-memory/qa-lead/MEMORY.md new file mode 100644 index 0000000000..c0208eaac3 --- /dev/null +++ b/.claude/agent-memory/qa-lead/MEMORY.md @@ -0,0 +1,5 @@ +- [Project Overview](project_overview.md) — game pillars that override default story-type gating for save/permadeath work +- [Save/Persistence GDD Status](project_save-persistence-status.md) — undesigned downstream systems; Temporizador↔Guardado reciprocity gap found 2026-08-09 +- [Troop System AC Status](project_troop-system-ac-status.md) — sistema-de-tropas.md AC draft status, Combate/Daño & Kaiju blockers, 2 undocumented dependency gaps +- [Heroes System AC Status](project_heroes-system-ac-status.md) — sistema-de-heroes.md AC written & approved; AC-H11/H25/H29 blocked on Permadeath & Kaiju GDDs +- [Input System AC Status](project_input-system-ac-status.md) — input.md AC17 vs. permadeath.md AC-P07c pause contradiction; AC22 not headlessly automatable; AC19-21 unclassified diff --git a/.claude/agent-memory/qa-lead/project_heroes-system-ac-status.md b/.claude/agent-memory/qa-lead/project_heroes-system-ac-status.md new file mode 100644 index 0000000000..8f5cf88182 --- /dev/null +++ b/.claude/agent-memory/qa-lead/project_heroes-system-ac-status.md @@ -0,0 +1,83 @@ +--- +name: project-heroes-system-ac-status +description: Status of Sistema de Héroes GDD's Acceptance Criteria and which ACs are blocked on undesigned Permadeath / Encuentro con Kaiju, as of 2026-08-06 +metadata: + type: project +--- + +`design/gdd/sistema-de-heroes.md` is the Pilar-1-eje GDD (hero permadeath / relic-quality +formula). As of 2026-08-06 its Acceptance Criteria section (AC-H01–AC-H32, Dado/Cuando/Entonces +format) was drafted, approved by the user, and written to the file. + +Same pattern as [[project-troop-system-ac-status]] and [[project-save-persistence-status]]: +several ACs are blocked on undesigned downstream GDDs and are only verifiable today via a +mocked contract at Sistema de Héroes' own boundary: +- **AC-H11** (`DYING → DEAD` only after the sustained beat releases) — blocked on Permadeath, + which owns the beat timing. Sistema de Héroes only owns entering/holding `DYING`. +- **AC-H25** (serialized death beats when two heroes die same frame — no two simultaneous + `DEATH_HOLD`s) — blocked on Permadeath, which owns serialization. Sistema de Héroes only + owns snapshotting each hero's D/T/P independently (that part, AC-H24, IS testable today). +- **AC-H29** (~30s tactical-decision window before an inevitable death changes the D/T/P + outcome — Test de Diseño 1) — blocked on Encuentro con Kaiju, which owns encounter design + and the kaiju timer. Verifiable today only with a mock encounter exposing the same hooks. + +Distinction worth preserving (mirrors the troop-system note): when a GDD *exposes* a contract +at its own boundary vs. *consumes/controls* a downstream system's behavior, the exposure side +is testable now even if the consumer is undesigned. **AC-H12** (Sistema de Héroes exposes the +full death-circumstance payload — hero_id, hero_name, relic_category, snapshotted D/T/P — to +Permadeath) is fully testable today via a mock listener, even though AC-H11/AC-H25 (what +Permadeath *does* with the beat timing) are not. + +**Why**: Sistema de Héroes is the load-bearing system for Pilar 1 (Sacrificio con Peso) — the +`relic_quality` gate formula (D as gate, T/w_stakes and P/w_purpose as bonuses) is the +mechanical proof that intention, not circumstance alone, determines legacy quality. The +negative-agency test (AC-H13/H14: D=0 caps quality at 0.10 regardless of T/P) is the single +most safety-critical unit test in this GDD — any refactor of the relic_quality formula should +re-run it first. + +**How to apply**: when Permadeath or Encuentro con Kaiju GDDs get authored, circle back to +`sistema-de-heroes.md` to backfill AC-H11, AC-H25, and AC-H29 against the real contracts (not +mocks), and confirm Permadeath's own GDD satisfies the exposure contract Sistema de Héroes +already committed to in AC-H12 (hero_id/hero_name/relic_category/D-T-P snapshot). Also check +whether Forja de Legado (also undesigned) needs to confirm it consumes the relic_quality→tier +mapping (AC-H19/AC-H20 boundaries) unchanged. + +**2026-08-07 adversarial pass — additional findings not yet actioned in the GDD file:** + +1. **AC-H21 belongs in the blocked-AC list too, and isn't tagged.** AC-H21 tests the + destination-match rule that decides `D` ("orden emitida con destino X pero murió en otro + lugar → D=0") — but the Dependencies table assigns *providing* the D classification to + Combate/Daño (OQ-2, undesigned). AC-H21 is only verifiable today via a mock Combate/Daño + that happens to implement exactly this destination-match rule — same structural risk as + AC-H11/H25/H29. It should carry a **(bloqueado)** tag and be added to this file's backfill + list, keyed to OQ-2, not just OQ-3/OQ-4. By contrast AC-H13/H16-H20/H22 are fine unblocked — + they test the `relic_quality` formula given D/T/P as opaque inputs, which is source-agnostic. + +2. **AC-H23 doesn't test joint-extreme knob combinations, and there's a real bug hiding here.** + `relic_quality` at D=1,T=1,P=1 reduces algebraically to exactly + `base_deliberate + w_stakes + w_purpose` (the `base_accidental` terms cancel). At the + **joint minimum** of each knob's individually-clamped safe range (0.30 + 0.20 + 0.15 = + 0.65), Tier 3 Legendaria (needs ≥0.70) becomes **mathematically unreachable even in the + best possible circumstance** — confirmed by hand calc, not yet caught by any AC. At the + **joint maximum** (0.50 + 0.40 + 0.30 = 1.20), the outer `clamp(...,0,1)` silently + saturates, flattening the intended distinction between "good" and "perfect" circumstances. + The Tuning Knobs "Enforcement" text only clamps individual knob values, not the sum — this + is a genuine spec gap (route to game-designer/systems-designer: decide whether to normalize + the three weights to always sum to 1.00, or reject config load if sum drifts too far) before + a new AC-H23b (joint-minimum reachability) / AC-H23c (joint-maximum saturation) can be + written definitively. This matches what systems-designer's parallel review is independently + checking on the troop/kaiju side — coordinate findings before either GDD gets a fix. + +3. **AC-H32 is now stale and should be split, not just refreshed.** UI Requirements + (U-1..U-7) are fully written as of this file's last read — the "placeholder, advisory not + gate" note under AC-H32 no longer applies. Split into AC-H32a (3-state discrete tier + transition, U-2), AC-H32b (visibility threshold gating — testable structurally today via a + mock threshold even though OQ-6's exact number is still open), AC-H32c (freeze-on-DYING, + U-4 — Integration tier, ties to the AC-H15 snapshot), AC-H32d (no editorializing text + present, U-5). Also: **U-6** (selection marker visually differentiates hero vs. troop) has + **no covering AC at all** — AC-H07 only tests selection *priority* logic, not the marker's + visual differentiation. Gap to close when AC-H32 gets rewritten. + +4. **Core Rule 4 (irreversibility) has a coverage gap.** AC-H27 only tests irreversibility + through a crash/reload. No AC tests an in-session attempt to act on a `DEAD` hero (heal + target validation, order issuance, etc.) being rejected/no-op. Propose AC-H27b. diff --git a/.claude/agent-memory/qa-lead/project_input-system-ac-status.md b/.claude/agent-memory/qa-lead/project_input-system-ac-status.md new file mode 100644 index 0000000000..bae395b78e --- /dev/null +++ b/.claude/agent-memory/qa-lead/project_input-system-ac-status.md @@ -0,0 +1,68 @@ +--- +name: project-input-system-ac-status +description: Adversarial review findings on design/gdd/input.md Acceptance Criteria (AC 1-25), as of 2026-08-19 — DEATH_HOLD/pause contradiction with Permadeath, AC22 not headlessly automatable, AC19-21 unclassified +metadata: + type: project +--- + +`design/gdd/input.md` Acceptance Criteria section (AC 1-25, GIVEN-WHEN-THEN) reviewed +adversarially on 2026-08-19. Same pattern as [[project-heroes-system-ac-status]] and +[[project-troop-system-ac-status]]: found a cross-GDD contract contradiction plus several +testability/coverage gaps not yet actioned in the file. + +**Cross-GDD contradiction (highest priority, confirmed against source):** +- `input.md` Edge Cases (line 105) states gamepad disconnect mid-game **unconditionally** + "pausa automáticamente el juego" (consola AND PC), with no `DEATH_HOLD` exception. +- `permadeath.md` Regla 4 / AC-P07c (line 81, 267-268) is explicit: during a `HOLDING` beat, + "ninguna pausa real del motor puede iniciarse" — the pause-menu action itself is suppressed, + `SceneTree.paused` must stay `false` the entire time. +- `input.md` AC 17 only covers the `RTS_COMMAND` case ("GIVEN contexto RTS_COMMAND (en consola + o PC)") — it never tests a disconnect while `DEATH_HOLD` is active, so the contradiction is + invisible to the AC suite even though the prose asserts behavior that would violate Permadeath's + own AC-P07c if implemented literally. +- **Fix**: add AC 17b — GIVEN `DEATH_HOLD` active (Permadeath `HOLDING`), WHEN the active gamepad + disconnects, THEN the reconnect-prompt signal still fires, but no real-engine-pause request is + emitted/honored (`SceneTree.paused` stays `false`) — any pause is deferred until Permadeath + releases the beat. Also patch the Edge Cases prose (line 105) to carve out this exception + explicitly rather than stating it as unconditional. + +**Other findings (not yet actioned):** +1. **AC 22 (performance/latency) is not automatable as a headless unit test.** It requires "cualquier + evento de input físico por el OS" measured over a real 60-second sample with 0 dropped/delayed + events — this needs hardware-in-the-loop / real OS input capture, which directly conflicts with + the project's own Testing Standards ("Determinism: no time-dependent assertions" and "What NOT + to Automate: platform-specific... test on target hardware, not headlessly"). The classification + table (line 225) tags it "Logic/perf" and BLOQUEANTE but never names an evidence location (every + other row in that table does). This is the single BLOCKING gate for Foundation sign-off and it's + currently ungated — recommend reclassifying as Integration/Performance with an instrumented + on-device test in `tests/performance/input/`, not `tests/unit/input/`. +2. **AC 3's exclusivity claim ("ningún otro contexto permanece activo simultáneamente") is only + operationalizable once you know the internal representation is a single `current_context` field** + — but that field name is only revealed later, in AC 4. As written, AC 3 alone doesn't tell a test + author what to assert. Minor doc-ordering fix: state the single-field representation once, in + Core Rule 2 or AC 3 itself. +3. **AC 4's "el cambio es rechazado" doesn't specify the rejection mechanism** (exception thrown vs. + silent no-op vs. assertion) — needed for a deterministic unit test assertion. +4. **Edge case coverage gap**: the Edge Cases prose bullet "Si un valor de zoom queda fuera de + `[zoom_min, zoom_max]` por un cambio de configuración... se hace clamp al rango válido en el + siguiente frame" has **no corresponding AC**. AC 24/25 only test the player-driven boundary case + (pressing zoom-in/out while already at the limit), not the external-cause case (a config change + putting zoom out of bounds, clamped on the next input frame). Propose AC 24b. +5. **AC 19-21 are missing from the story-type classification table** (line 222-226). The table + only buckets DEATH_HOLD edge cases, gamepad disconnect, input-method-mid-action, performance, and + AC 23-25 — it never assigns a type/gate to AC 19 (pause during `BLESSING_SELECT`), AC 20 (button + conflict resolved by context), or AC 21 (click on unit border). Per this project's hard + Logic/Integration test-evidence gate, an unclassified AC has no defined evidence requirement and + could silently ship without test coverage. All three read as Logic (pure state-machine/routing + behavior, no external system needed) — recommend adding them to the Logic bucket explicitly. + +**Open Questions cross-check**: the one fully-open question ("¿El gamepad es opcional en PC o se +asume conectado?... falta definir el flujo de 'jugar sin gamepad conectado' en PC") does not +block any of the 25 existing ACs — AC 17 presupposes a gamepad was already connected and then +disconnects, which is well-defined regardless of the open question. But note the flow the open +question describes (booting the game on PC with no gamepad ever connected) has **zero AC +coverage** today — worth flagging so whoever resolves the open question also adds the AC. + +**How to apply**: when `input.md` is revised, check that AC 17b lands and cross-references +`permadeath.md` AC-P07c by name (mirrors how `permadeath.md` itself cross-references Input/Combate/ +Kaiju contracts). Re-run this check if `permadeath.md` Regla 4 changes. diff --git a/.claude/agent-memory/qa-lead/project_overview.md b/.claude/agent-memory/qa-lead/project_overview.md new file mode 100644 index 0000000000..03280e3f5e --- /dev/null +++ b/.claude/agent-memory/qa-lead/project_overview.md @@ -0,0 +1,16 @@ +--- +name: project-overview +description: Core facts about "What the Gods Left Behind" needed to classify story types and judge testability gates +metadata: + type: project +--- + +Game: "What the Gods Left Behind" — RTS/roguelite, Godot 4.6, GDScript. Two design pillars drive QA gating decisions: +- **Pilar 1 — Sacrificio con Peso**: permadeath must be truly irreversible; no reload-to-undo. Any system touching save/load or hero death is treated as data-integrity-critical regardless of its normal story-type classification. +- **Pilar 2 — El Pasado es Poder**: the player's relic pantheon must survive across sessions; loss of pantheon data on close/crash is treated as a Pillar-breaking bug (S1), not a normal data bug. + +**Why**: these two pillars are the reason the studio's default "Config/Data = advisory" rule gets overridden for anything checksum/atomic-write/schema-version related in the save system — see [[project-save-persistence-status]]. + +**How to apply**: when classifying story types for QA plans or evaluating gate blockers, treat any story touching save/load, permadeath resolution, or relic/pantheon persistence as at least Integration/blocking, and escalate to blocking even for what would normally be Config/Data if it affects irreversibility or pantheon durability. + +GDDs live in `design/gdd/`. Standard 8-section structure is enforced by `design/CLAUDE.md` and `.claude/rules/design-docs.md`. diff --git a/.claude/agent-memory/qa-lead/project_save-persistence-status.md b/.claude/agent-memory/qa-lead/project_save-persistence-status.md new file mode 100644 index 0000000000..8e0ea37a50 --- /dev/null +++ b/.claude/agent-memory/qa-lead/project_save-persistence-status.md @@ -0,0 +1,21 @@ +--- +name: project-save-persistence-status +description: Status of the Guardado/Persistencia GDD, which downstream systems are still undesigned, and known dependency-reciprocity gaps, as of 2026-08-09 +metadata: + type: project +--- + +`design/gdd/guardado-persistencia.md` is a Foundation-layer GDD (no upstream deps). Status field: **"In Design"** (not Approved) as of 2026-08-09. Its Acceptance Criteria section is now written to the file (Given-When-Then, AC-01 through at least AC-35, e.g. AC-33 covers same-frame concurrent autosave serialization) — the "pending approval, not yet written" note from 2026-07-29 is stale, superseded. + +Downstream systems that hard-depend on it are **all still undesigned as GDDs**, with Datos de Era/Civilización as a partial exception (it has a documented bidirectional relationship with Guardado, but Datos de Era's own Status is also "In Design", not Approved — see [[project-heroes-system-ac-status]] pattern of not over-crediting "In Design" GDDs as stable): +- Permadeath — undesigned +- Forja de Legado — undesigned +- Transición de Era — undesigned +- Salón Conmemorativo/Panteón — undesigned +- UI/HUD — undesigned + +**Why**: several acceptance criteria for the save system's Core Rule 1 (mandatory autosave on hero death / relic forge / era complete) and Core Rule 2 (manual save UI gating) can only be unit-tested today via a mocked trigger call into the Save system's own API. The true end-to-end integration test (real hero-death event → real autosave) is blocked until Permadeath/Forja de Legado/Transición de Era/UI-HUD GDDs exist and define their trigger signal contracts. + +**Known reciprocity gap (found 2026-08-09, during `temporizador-de-preparacion-ritual.md` review)**: that GDD claims a **bidirectional** hard dependency on Guardado/Persistencia (elapsed_s + current_phase are "part of the saveable state," persisted on autosave, phase recomputed via its own formula F3 on load — its AC-T23 tests this). `guardado-persistencia.md` does **not** mention Temporizador de Preparación/Ritual anywhere (no dependency-table row, no elapsed_s/current_phase field, no interface). This violates the project's own rule (`.claude/rules/design-docs.md`: "Dependencies must be bidirectional — if system A depends on B, B's doc must mention A"). Practical effect: Temporizador's AC-T23 (save/load phase recompute) rests on a one-sided, unconfirmed persistence schema, but — unlike that GDD's AC-T03b/T05b/T07b — it is **not** tagged "(bloqueado)" with a Mock Contract Assumptions note, which is an inconsistent application of the project's own established blocked-AC pattern (compare Guardado's own AC-01b, or Héroes AC-H11/AC-H25). + +**How to apply**: when any of the five undesigned GDDs above get authored, flag their save-system integration hooks and circle back to backfill the blocked integration-level acceptance criteria in `guardado-persistencia.md` (marked "-b" variants in the AC draft, e.g. AC-01b/02b/03b, plus AC-07, AC-09, TK4 UI-display verification). Don't let those GDDs ship without confirming they satisfy the Save system's already-drafted contract (autosave must complete before proceeding to next screen/state; manual save UI must not expose the option during Clímax de Combate / DEATH_HOLD). Additionally: when `guardado-persistencia.md` is next revised, check whether it has added a reciprocal Temporizador de Preparación/Ritual row/field — if not, this is still an open reciprocity gap to flag in any review touching either file. diff --git a/.claude/agent-memory/qa-lead/project_troop-system-ac-status.md b/.claude/agent-memory/qa-lead/project_troop-system-ac-status.md new file mode 100644 index 0000000000..56fac46617 --- /dev/null +++ b/.claude/agent-memory/qa-lead/project_troop-system-ac-status.md @@ -0,0 +1,50 @@ +--- +name: project-troop-system-ac-status +description: Status of Sistema de Tropas GDD's Acceptance Criteria and dependency gaps found while drafting them, as of 2026-08-06 +metadata: + type: project +--- + +`design/gdd/sistema-de-tropas.md` is a Core-layer GDD. As of 2026-08-06 its Acceptance +Criteria section was drafted (~30 Given-When-Then criteria across Core Rules, States, +Formulas, Edge Cases, Tuning Knobs) but not yet written to the file — pending user approval. + +Same pattern as [[project-save-persistence-status]]: several ACs are blocked on undesigned +downstream GDDs (Combate/Daño, Encuentro con Kaiju) and can only be verified today via +mocked signals at the Troop system's boundary: +- MOVING/IDLE→ENGAGED and ENGAGED→IDLE transitions — blocked on Combate/Daño's + contact/resolution event contract (not yet defined). +- Overkill health-clamp-to-0 — blocked on Combate/Daño owning the actual clamp operation + (Troop system only owns the `is_dead` threshold reaction, which IS testable today). +- "All troops dead, hero alive → era continues" (no total-troop-loss defeat) — blocked, AND + flagged as a genuine dependency-table gap: no system in `sistema-de-tropas.md`'s + Dependencies section currently claims ownership of era win/loss evaluation. Likely + Transición de Era or Encuentro con Kaiju, both undesigned. + +Two additional gaps found (not previously tracked anywhere): +1. **Preparación-phase dependency is undocumented.** Core Rule 2 and multiple Edge Cases + reference a "fase de Preparación" with a timer boundary ("antes de que el temporizador + del kaiju inicie"), but no system owning that phase/timer state is listed in the + Dependencies table. The recruit-cap UI-gating AC is therefore only partially testable + (cap-enforcement logic yes, UI/phase gating no) until this is resolved. +2. **Encuentro con Kaiju's "troops as targets / exposes positions" interface has zero + corresponding Core Rule or Edge Case** anywhere in the GDD body — it's asserted only in + the Dependencies/Interactions tables. When Encuentro con Kaiju is designed, expect + `sistema-de-tropas.md` to need a new Core Rule or Edge Case (not just a downstream + interface row) specifying what "exposes positions" means mechanically (polling API? + signal-per-move? radius-limited query?). +3. Also noted: Guardado/Persistencia is fully designed and troop count/state is confirmed + as save-relevant, but no round-trip save/load AC currently exists for troops. This is + testable today (both systems designed) — recommend adding it as a coverage gap fix + rather than a blocked item. + +**Why**: `sistema-de-tropas.md` explicitly implements Pilar 1 (Sacrificio con Peso) by +being the mechanical *opposite* of hero permadeath — Core Rule 6 (troop death does NOT +capture circumstance data or trigger legacy) is the load-bearing negative test protecting +that contrast. Treat any future refactor touching both hero-death and troop-death code +paths as high-risk for silently merging the two and breaking the pillar. + +**How to apply**: when Combate/Daño or Encuentro con Kaiju GDDs get authored, circle back +to `sistema-de-tropas.md` to backfill the blocked ACs (ENGAGED transitions, overkill clamp, +total-troop-loss non-defeat) and confirm the two documented gaps above got resolved in +those new GDDs' Dependencies sections. diff --git a/.claude/agent-memory/systems-designer/MEMORY.md b/.claude/agent-memory/systems-designer/MEMORY.md new file mode 100644 index 0000000000..3941497b9a --- /dev/null +++ b/.claude/agent-memory/systems-designer/MEMORY.md @@ -0,0 +1 @@ +- [Project context: What the Gods Left Behind](project_wtgleft_context.md) — pillars, GDD set, DEATH_HOLD/autosave coupling note diff --git a/.claude/agent-memory/systems-designer/project_wtgleft_context.md b/.claude/agent-memory/systems-designer/project_wtgleft_context.md new file mode 100644 index 0000000000..5e3e9bc0c8 --- /dev/null +++ b/.claude/agent-memory/systems-designer/project_wtgleft_context.md @@ -0,0 +1,32 @@ +--- +name: project-wtgleft-context +description: Core facts about "What the Gods Left Behind" (Godot 4.6 RTS/roguelite) — pillars, GDD set, and cross-system coupling notes relevant to systems design work +metadata: + type: project +--- + +The project is "What the Gods Left Behind" — a Godot 4.6 RTS/roguelite. Core pillars: +1. Sacrificio con Peso — permadeath must be truly irreversible, no reload-to-undo. +2. El Pasado es Poder — legacy/pantheon (forged relics, era history) must survive between sessions. +3. Preparación Ritual, Clímax Explosivo — pacing alternates safe prep beats and combat climax beats. +4. Historia Jugada, No Contada — story emerges from play, not exposition. + +GDDs authored so far (design/gdd/): game-concept.md, input.md, datos-de-era-civilizacion.md, +systems-index.md, guardado-persistencia.md (Guardado/Persistencia — Foundation layer, save system). + +**Why:** These pillars are the arbitration standard for every systems-design tradeoff on this +project — e.g., save/retry failure handling must never silently let a permadeath event go +unpersisted (violates Pillar 1 from a different angle: a crash before a successful autosave +would let a dead hero reappear on reload). + +**How to apply:** When designing any new formula or rule for this project, check it against +these four pillars before proposing defaults. Re-read the specific GDD for exact wording rather +than trusting this summary, since pillars phrasing may be refined over time. + +Known cross-system coupling note: the Input GDD (`design/gdd/input.md`) defines a `DEATH_HOLD` +input context (all player input suppressed during the permadeath beat) but deliberately does +NOT define its duration — that's owned by a not-yet-authored Permadeath GDD. The +Guardado/Persistencia GDD already specifies that the mandatory autosave on hero death must +complete (blocking) *before* `DEATH_HOLD` is released, so `DEATH_HOLD`'s minimum duration is +implicitly save-write-time + whatever pacing buffer the Permadeath GDD authors want — no +separate timing formula is needed to couple them, sequencing already guarantees it. diff --git a/.claude/docs/technical-preferences.md b/.claude/docs/technical-preferences.md index 94a91fbb02..48a8f2f8e9 100644 --- a/.claude/docs/technical-preferences.md +++ b/.claude/docs/technical-preferences.md @@ -5,58 +5,55 @@ ## Engine & Language -- **Engine**: [TO BE CONFIGURED — run /setup-engine] -- **Language**: [TO BE CONFIGURED] -- **Rendering**: [TO BE CONFIGURED] -- **Physics**: [TO BE CONFIGURED] +- **Engine**: Godot 4.6 +- **Language**: GDScript +- **Rendering**: Forward+ (Godot 4 default) +- **Physics**: Jolt (Godot 4.6 default) ## Input & Platform -- **Target Platforms**: [TO BE CONFIGURED — e.g., PC, Console, Mobile, Web] -- **Input Methods**: [TO BE CONFIGURED — e.g., Keyboard/Mouse, Gamepad, Touch, Mixed] -- **Primary Input**: [TO BE CONFIGURED — the dominant input for this game] -- **Gamepad Support**: [TO BE CONFIGURED — Full / Partial / None] -- **Touch Support**: [TO BE CONFIGURED — Full / Partial / None] -- **Platform Notes**: [TO BE CONFIGURED — any platform-specific UX constraints] +- **Target Platforms**: PC, Console +- **Input Methods**: Keyboard/Mouse (primario), Gamepad (consola) +- **Primary Input**: Keyboard/Mouse +- **Gamepad Support**: Full +- **Touch Support**: None +- **Platform Notes**: El esquema de control está diseñado mouse-first (selección de unidades, cámara RTS). El soporte de gamepad para consola necesitará un sistema de selección/cursor alternativo diseñado explícitamente, no solo un remapeo directo. ## Naming Conventions -- **Classes**: [TO BE CONFIGURED] -- **Variables**: [TO BE CONFIGURED] -- **Signals/Events**: [TO BE CONFIGURED] -- **Files**: [TO BE CONFIGURED] -- **Scenes/Prefabs**: [TO BE CONFIGURED] -- **Constants**: [TO BE CONFIGURED] +- **Classes**: PascalCase (e.g., `PlayerController`) +- **Variables**: snake_case (e.g., `move_speed`) +- **Signals/Events**: snake_case, tiempo pasado (e.g., `health_changed`) +- **Files**: snake_case coincidiendo con la clase (e.g., `player_controller.gd`) +- **Scenes/Prefabs**: PascalCase coincidiendo con el nodo raíz (e.g., `PlayerController.tscn`) +- **Constants**: UPPER_SNAKE_CASE (e.g., `MAX_HEALTH`) ## Performance Budgets -- **Target Framerate**: [TO BE CONFIGURED] -- **Frame Budget**: [TO BE CONFIGURED] -- **Draw Calls**: [TO BE CONFIGURED] -- **Memory Ceiling**: [TO BE CONFIGURED] +- **Target Framerate**: 60 FPS +- **Frame Budget**: 16.6ms +- **Draw Calls**: <1000 (2D con sprite batching) +- **Memory Ceiling**: [TO BE CONFIGURED — definir cuando se conozca el hardware objetivo] ## Testing -- **Framework**: [TO BE CONFIGURED] +- **Framework**: GdUnit4 (Godot 4 test framework) — runner `tests/gdunit4_runner.gd`, CI `.github/workflows/tests.yml` (gdUnit4-action). Instalar vía AssetLib → `res://addons/gdunit4/`. *(Corregido 2026-08-21: antes decía "GUT" por error; el comando de CI de coding-standards y `/test-setup` siempre usaron GdUnit4.)* - **Minimum Coverage**: [TO BE CONFIGURED] - **Required Tests**: Balance formulas, gameplay systems, networking (if applicable) ## Forbidden Patterns - - [None configured yet — add as architectural decisions are made] ## Allowed Libraries / Addons - -- [None configured yet — add as dependencies are approved] +- **GdUnit4** (test framework, dev-only) — aprobado 2026-08-21 vía `/test-setup`. No se envía en el build de release. ## Architecture Decisions Log - - [No ADRs yet — use /architecture-decision to create one] ## Engine Specialists @@ -65,12 +62,12 @@ -- **Primary**: [TO BE CONFIGURED — run /setup-engine] -- **Language/Code Specialist**: [TO BE CONFIGURED] -- **Shader Specialist**: [TO BE CONFIGURED] -- **UI Specialist**: [TO BE CONFIGURED] -- **Additional Specialists**: [TO BE CONFIGURED] -- **Routing Notes**: [TO BE CONFIGURED] +- **Primary**: godot-specialist +- **Language/Code Specialist**: godot-gdscript-specialist (all .gd files) +- **Shader Specialist**: godot-shader-specialist (.gdshader files, VisualShader resources) +- **UI Specialist**: godot-specialist (no dedicated UI specialist — primary covers all UI) +- **Additional Specialists**: godot-gdextension-specialist (GDExtension / native C++ bindings only) +- **Routing Notes**: Invoke primary for architecture decisions, ADR validation, and cross-cutting code review. Invoke GDScript specialist for code quality, signal architecture, static typing enforcement, and GDScript idioms. Invoke shader specialist for material design and shader code. Invoke GDExtension specialist only when native extensions are involved. ### File Extension Routing @@ -79,9 +76,9 @@ | File Extension / Type | Specialist to Spawn | |-----------------------|---------------------| -| Game code (primary language) | [TO BE CONFIGURED] | -| Shader / material files | [TO BE CONFIGURED] | -| UI / screen files | [TO BE CONFIGURED] | -| Scene / prefab / level files | [TO BE CONFIGURED] | -| Native extension / plugin files | [TO BE CONFIGURED] | -| General architecture review | Primary | +| Game code (.gd files) | godot-gdscript-specialist | +| Shader / material files (.gdshader, VisualShader) | godot-shader-specialist | +| UI / screen files (Control nodes, CanvasLayer) | godot-specialist | +| Scene / prefab / level files (.tscn, .tres) | godot-specialist | +| Native extension / plugin files (.gdextension, C++) | godot-gdextension-specialist | +| General architecture review | godot-specialist | diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml new file mode 100644 index 0000000000..60b077828c --- /dev/null +++ b/.github/workflows/tests.yml @@ -0,0 +1,37 @@ +name: Automated Tests + +on: + push: + branches: [main] + pull_request: + branches: [main] + +jobs: + test: + name: Run GdUnit4 Tests + runs-on: ubuntu-latest + + steps: + - name: Checkout + uses: actions/checkout@v4 + with: + lfs: true + + - name: Run GdUnit4 Tests + uses: MikeSchulze/gdUnit4-action@v1 + with: + # Godot 4.6 (see docs/engine-reference/godot/VERSION.md). + # If the action rejects '4.6', use the full patch string it expects + # (e.g. '4.6.0' / '4.6-stable'). + godot-version: '4.6' + paths: | + tests/unit + tests/integration + report-name: test-results + + - name: Upload Test Results + if: always() + uses: actions/upload-artifact@v4 + with: + name: test-results + path: reports/ diff --git a/.gitignore b/.gitignore index 5830aabdc7..20b5034aa4 100644 --- a/.gitignore +++ b/.gitignore @@ -13,6 +13,7 @@ Desktop.ini .claude/settings.local.json CLAUDE.local.md production/session-logs/ +production/session-state/ # === Internal Planning (not for public) === diff --git a/CLAUDE.md b/CLAUDE.md index d365d5ee66..94809eccbe 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -5,11 +5,11 @@ Each agent owns a specific domain, enforcing separation of concerns and quality. ## Technology Stack -- **Engine**: [CHOOSE: Godot 4 / Unity / Unreal Engine 5] -- **Language**: [CHOOSE: GDScript / C# / C++ / Blueprint] +- **Engine**: Godot 4.6 +- **Language**: GDScript - **Version Control**: Git with trunk-based development -- **Build System**: [SPECIFY after choosing engine] -- **Asset Pipeline**: [SPECIFY after choosing engine] +- **Build System**: SCons (engine), Godot Export Templates +- **Asset Pipeline**: Godot Import System + custom resource pipeline > **Note**: Engine-specialist agents exist for Godot, Unity, and Unreal with > dedicated sub-specialists. Use the set matching your engine. diff --git a/design/art/art-bible.md b/design/art/art-bible.md new file mode 100644 index 0000000000..d192b95dae --- /dev/null +++ b/design/art/art-bible.md @@ -0,0 +1,484 @@ +# Art Bible: What the Gods Left Behind + +## Document Status +- **Version**: 1.0 +- **Last Updated**: 2026-07-23 +- **Owned By**: art-director +- **Status**: Draft (todas las 9 secciones completas) +- **Art Director Sign-Off (AD-ART-BIBLE)**: Omitido — Modo lean (no es un PHASE-GATE) + +--- + +## 1. Visual Identity Statement + +### 1.1 Visual Identity Anchor + +**"Gótico Pixelado Sagrado" (Sacred Pixel Gothic)** + +> Cada escena debe leerse como un fresco de vitral renderizado en píxeles — ornamentado, oscuro y reverente. Nada en pantalla es decoración; todo es una reliquia esperando ser ganada, o una advertencia de lo que cuesta. + +### 1.2 Principios Visuales de Soporte + +**Principio A — El Legado es Luz.** +El color no es decorativo, es un indicador de estado: la paleta apagada de piedra/sangre/hueso es el estado por defecto del mundo, y el color dorado/luminoso se reserva exclusivamente para cualquier elemento que se originó de una muerte permanente — reliquias, bendiciones, retratos de héroes en el salón conmemorativo, el momento mismo de la caída de un héroe. +*Test de diseño*: Al decidir si un elemento debe tener tratamiento dorado luminoso, preguntar "¿esto existe porque un héroe murió por ello?" Si no, se queda apagado — sin excepciones por conveniencia de legibilidad o pedidos de "que resalte más". +*Ancla de pilares*: **Sacrificio con Peso** y **El Pasado es Poder** — la paleta es el mecanismo por el cual el ojo del jugador aprende, partida tras partida, que la muerte es la única fuente de verdadero brillo visual (y mecánico) en este mundo. + +**Principio B — La Escala Dice Quién Puede Ganar.** +Los kaijus y otras amenazas de apuesta permanente deben ocupar una escala abrumadora y dominante en pantalla contra unidades góticas pequeñas y ornamentadas; el contraste nunca se suaviza por claridad de combate. +*Test de diseño*: Si un encuentro con kaiju permite que se lea como "solo un enemigo grande" en vez de "una entidad que tu civilización no puede superar en escala, solo resistir", se recompone la escena/nivel, no se encoge al kaiju. +*Ancla de pilares*: **Preparación Ritual, Clímax Explosivo** — la brecha de escala es lo que hace que el clímax se sienta explosivo en vez de una escaramuza RTS normal. + +**Principio C — La Cámara Nunca Aparta la Mirada de la Decisión.** +Los momentos donde el jugador elige la muerte de un héroe (o esta se resuelve por su acción) deben escenificarse con siluetas legibles y sin obstrucciones, con una pausa compositiva deliberada — sin corte a negro, sin cutscene pasiva, sin que la cámara titubee. +*Test de diseño*: Si un momento de muerte/sacrificio puede resolverse fuera de pantalla, en un menú, o vía una cutscene pasiva en vez de un beat enmarcado y disparado por el jugador dentro del mundo, se rediseña la escenificación hasta que la cámara sostenga la mirada. +*Ancla de pilares*: **Historia Jugada, No Contada** — protege también **Sacrificio con Peso**: una muerte de la que la cámara aparta la mirada se lee como desechable, no definitiva. + +### 1.3 Anclaje de Referencias + +- **Blasphemous 2** informa el **Principio A**: su disciplina de iluminación pictórica dentro de una grilla estricta de píxeles es la prueba de que "base apagada + acento luminoso reservado" puede leerse como sagrado y no solo como bajo contraste. Tomamos su disciplina de iluminación, no su paleta específica. +- **Dark Souls** informa el **Principio B**: su storytelling ambiental a través de la escala (el personaje del jugador como una figura pequeña y sobre-armada empequeñecida por la arquitectura y los jefes) es el modelo directo de cómo deben sentirse los encuentros con kaijus. +- **Hollow Knight** informa el **Principio C**: su contención al escenificar momentos emocionalmente pesados — sostener una silueta quieta y legible en espacio negativo en vez de cortar — es la referencia para cómo deben componerse los momentos de muerte/decisión. + +--- + +## 2. Mood & Atmosphere + +### 2.1 Preparación (Ritual Build-Up) + +- **Mood principal**: Reverencia ansiosa — el temor específico de disponer una ofrenda que sabes que quizás no sea suficiente, como colocar reliquias en un altar antes de un rito cuyo desenlace no controlas. +- **Carácter de iluminación**: Dirección de "última luz" de atardecer — ángulo de sol bajo, sombras largas y oblicuas de unidades y estandartes. Temperatura ámbar-óxido cálida en el suelo, enfriándose bruscamente hacia el azul en el horizonte donde es visible la aproximación del kaiju. Contraste medio-bajo, de bordes suaves. +- **Descriptores atmosféricos**: silencioso, votivo, deliberado, que se acerca, iluminado por velas. +- **Nivel de energía**: Medido, casi litúrgico — los paneos de cámara son lentos, la colocación de unidades tiene peso. +- **Elemento visual concreto**: La silueta del kaiju que se acerca en el horizonte se renderiza como un fragmento de vitral en movimiento — no anima como criatura todavía, se *arrastra* como una sombra cruzando un reloj de sol, y la dirección de sombra de cada unidad se actualiza para rastrearlo. *(Pilar 3, hecho diegético)* + +### 2.2 Clímax de Combate (Confrontación con el Kaiju) + +- **Mood principal**: Asombro abrumador — no pánico-horror, sino la pequeñez específica de estar bajo algo que nunca estuvo destinado a pelearse en igualdad. Cada golpe conectado debe sentirse robado, no ganado fácilmente. +- **Carácter de iluminación**: La dirección se invierte respecto a Preparación — la luz ahora viene desde detrás o a través del kaiju (destellos a contraluz de sus ataques), forzando a las unidades a casi-silueta. Temperatura fría dominante (azules amoratados, violetas ceniza) puntuada por destellos calientes. Contraste alto, de bordes duros — el principio del "fresco" en su forma más literal. +- **Descriptores atmosféricos**: sísmico, sacrílego, frenético, ensordecedor (visualmente), fugaz. +- **Nivel de energía**: Frenético pero breve — señala visualmente que no puede sostenerse, reforzando la cláusula "breve y explosivo" del Pilar 3. +- **Elemento visual concreto**: Los ataques individuales del kaiju dejan grietas de vidrio quemado a través del propio borde de la pantalla (una ventana de vitral fracturándose), que no sanan completamente entre encuentros dentro de la misma era — un marcador visual persistente de cicatrices. *(Principio B)* + +### 2.3 Muerte/Sacrificio de Héroe (Decisión de Legado) + +- **Mood principal**: Quietud sagrada — el único momento del juego que debe sentirse como si el tiempo se hubiera detenido deliberadamente para el jugador, no arrebatado de él. Duelo con agencia, según el Pilar 1. +- **Carácter de iluminación**: Cambio súbito a una única luz clave motivada — un haz o "luz divina" que aísla al héroe caído del combate circundante, que se desatura y desenfoca. Ambiente neutro-frío con una sola fuente cálida sobre el héroe (la única calidez restante en cuadro, anticipando el dorado por venir). Contraste extremo — casi-silueta contra luz sobreexpuesta, según el Principio C. +- **Descriptores atmosféricos**: silencioso, suspendido, fúnebre, sin titubeos, luminoso (solo localmente). +- **Nivel de energía**: Contemplativo, sostenido — el único estado donde la cámara y la simulación deben pausarse o ir en cámara lenta visiblemente, nunca cortar. +- **Elemento visual concreto**: La silueta del héroe se agrieta en fragmentos de vitral exactamente como un rosetón haciéndose añicos, y cada fragmento se congela en el aire en vez de caer de inmediato — el jugador tiene un instante para ver la forma antes de que se resuelva en el ícono de la reliquia (con borde dorado, Principio A). + +### 2.4 Salón Conmemorativo / Panteón + +- **Mood principal**: Orgullo solemne — una sala silenciosa como catedral donde el duelo se ha metabolizado en sentido; el jugador debe sentir el peso de una colección, no la satisfacción de un trofeo. +- **Carácter de iluminación**: Interior, indirecta — luz difusa como filtrada a través de vitrales reales sobre piso de piedra (charcos de luz de color proyectados por cada reliquia). Ambiente frío de piedra como base, calidez únicamente emanando de los objetos-reliquia. Contraste bajo-medio ambiental, alto localmente alrededor de cada reliquia. +- **Descriptores atmosféricos**: reverente, cavernoso, quieto, acumulativo, consagrado. +- **Nivel de energía**: Contemplativo — el ritmo más lento del juego, deliberadamente anticlimático. +- **Elemento visual concreto**: Los nichos vacíos de héroes aún no perdidos/reliquias aún no ganadas se renderizan como huecos de piedra oscuros y sin iluminar (no como placeholders de UI en gris) — la ausencia tiene la misma presencia material que la presencia. + +### 2.5 Transición de Era + +- **Mood principal**: Continuidad sombría — el desasosiego específico de un umbral: el final de una civilización es legible en la luz de apertura de la siguiente, así el jugador siente consecuencia, no un reinicio limpio. +- **Carácter de iluminación**: Un fundido lento y no simétrico en vez de un corte duro — la temperatura de color dominante de la era saliente se filtra visiblemente hacia el cuadro de apertura de la nueva era antes de corregirse a la paleta nativa de esa era. Contraste cambiante, inestable durante la transición misma. +- **Descriptores atmosféricos**: liminal, heredado, erosionándose, presagioso, tipo palimpsesto. +- **Nivel de energía**: Medido, casi geológico. +- **Elemento visual concreto**: Una grieta o costura literal (plomo de vitral) atraviesa el arte de transición conectando el cuadro final de la era vieja con el primer cuadro de la nueva era — el mismo motivo de fractura del estado de muerte de héroe, escalado a nivel civilizacional. *(Pilar 2)* + +### 2.6 Victoria de Era + +- **Mood principal**: Alivio costoso — nunca triunfal. La sensación específica de una supervivencia que requirió amputación: estás de pie, pero puedes ver lo que falta. +- **Carácter de iluminación**: Luz con dirección de amanecer (contrapunto formal al atardecer de Preparación), pero notablemente más dura y pálida que una calidez de "victoria" convencional — luz dorada presente solo donde se representan reliquias/héroes ganados, todo lo demás permanece en el apagado por defecto (Principio A). Contraste medio, con distribución deliberadamente desigual. +- **Descriptores atmosféricos**: raído, agridulce, cicatrizado, de pie, sin resolver. +- **Nivel de energía**: Contemplativo a medido — explícitamente no triunfal-frenético. +- **Elemento visual concreto**: La composición de victoria enmarca a la civilización sobreviviente desde *detrás* de sus propios monumentos cicatrizados de guerra y nichos conmemorativos vacíos en primer plano, así el jugador no puede ver "victoria" sin ver simultáneamente lo que costó. *(Extensión del Principio C a la victoria, no solo a la muerte)* + +## 3. Shape Language + +### 3.1 Filosofía de Silueta de Personajes + +A escala RTS — muchas unidades en pantalla simultáneamente, vistas desde una cámara top-down/isométrica — la silueta es lo único que el jugador procesa en menos de un segundo. El detalle muere a esta escala; la forma sobrevive. Cada arquetipo necesita una firma de forma legible a 24-32px, antes de que el color o la animación se resuelvan. + +**Tropa rasa — la astilla vertical.** Rectángulo angosto y erguido con un pico repetible (punta de lanza, punta de estandarte, punta de yelmo). Las tropas son deliberadamente indiferenciadas entre sí — esto es intencional. Según el Principio A (El Legado es Luz), solo lo ganado a través del sacrificio merece distinción visual; la tropa rasa es el default no ganado, el costo mortal que el jugador siempre está gastando. + +**Héroe — la silueta interrumpida.** Comparte la gramática vertical básica de la tropa (para seguir leyéndose como "una unidad de mi ejército") pero la rompe con un elemento asimétrico y sobresaliente: un brazo con arma en alto, una esquina de capa que arrastra, un estandarte fuera de eje, una silueta de reliquia irregular cargada a la espalda. La ruptura es el punto — un campo uniforme de astillas-tropa con una silueta rota adentro activa tanto figura-fondo como el principio de anomalía a la vez: el ojo se fija en la irregularidad dentro de la regularidad, sin necesitar color. Esto sirve directamente al Pilar 1 (Sacrificio con Peso) — el héroe debe distinguirse visualmente antes de que el jugador tenga apego alguno, para que cuando la ruptura de silueta desaparezca (el héroe muere), el jugador sienta que el campo vuelve a ser uniforme. + +**Kaiju — el que rompe el horizonte.** No una versión más grande de una forma de tropa, sino una categoría de forma fundamentalmente distinta: masa baja y ancha, pegada al suelo, con elementos de silueta que perforan por completo la banda de lectura de "unidad" (espinas, alas, una cabeza que se posiciona por encima de lo que cualquier encuadre de cámara o UI espera que ocupe "una unidad"). Según el Principio B, la forma del kaiju nunca debe resolverse como "una tropa muy grande" — debe leerse más como *terreno que se mueve* que como *un combatiente*. + +**Esbirros/minions (confirmado que existen):** heredan la familia de forma del kaiju en miniatura — angulares, quitinosos, de perfil bajo — en vez de la familia vertical de héroe/tropa. Esto mantiene "nuestro vs. suyo" legible como distinción de categoría de forma, no solo de color (crítico para accesibilidad de daltonismo y para legibilidad en el caos de alto contraste del Clímax de Combate). + +### 3.2 Geometría del Entorno + +**Lenguaje dominante: angular y vertical, de derivación gótica** — arcos apuntados, gabletes empinados, líneas de fractura dentadas — con la curvatura apareciendo solo como acento deliberado y raro, nunca como lenguaje estructural base. + +La angularidad es el hilo conductor que mantiene cada era sintiéndose como "un solo juego": formas puntiagudas, quebradas, que se alzan hacia arriba se leen como reverentes-y-peligrosas en cualquier era, ya sea un megalito de la Edad de Bronce con un capitel fracturado o una catedral medieval con bóveda de crucería. Las curvas — una piedra de río desgastada, una cúpula de osario redondeada, un estandarte combado — se reservan para lugares donde el juego quiere un respiro de *descanso* dentro del pavor: santuarios, terreno conmemorativo, zonas seguras de preparación. + +**Diferenciación por era sin perder unidad — "la silueta rota se repite, el vocabulario de ornamento cambia":** cada era-civilización debe cambiar *qué* produce la gramática angular (megalitos de poste-y-dintel y piedra agrietada por fuego en un capítulo de Edad de Bronce; arbotantes y ventanales con tracería en un capítulo medieval; fracturas de concreto/varilla o brutalismo geométrico destrozado en un capítulo posterior) mientras la regla subyacente — verticales dentadas dominan, la curvatura está racionada y es significativa — permanece constante. Esto extiende directamente el Pilar 2 (El Pasado es Poder): si reliquias/bendiciones de eras anteriores persisten como props literales o decoración de altar en entornos de eras posteriores, su vocabulario angular más antiguo sentado dentro de uno más nuevo se convierte en una línea de tiempo legible y sin palabras de lo que se ha sacrificado hasta ahora. + +**El territorio del kaiju obtiene su propia corrupción geométrica:** el suelo donde el kaiju se para o que ha dañado debe romper visiblemente el orden angular-gótico de la era — mampostería agrietada, rejillas de baldosas pandeadas, tajos orgánicos cortando a través de arquitectura de otro modo rígida. Herramienta directa de narrativa ambiental (Pilar 4): el jugador lee "este kaiju ya estuvo aquí" solo por la geometría, sin necesitar cutscene. + +### 3.3 Gramática de Forma de UI + +**Decisión: UI completamente diegética.** Toda la interfaz — incluyendo barras de vida, cooldowns e iconos de estado — sigue el lenguaje ornamentado gótico de vitral, sin una capa de geometría de información "neutra" separada. + +*Tradeoff reconocido explícitamente:* esto prioriza la inmersión y refuerza "nada en pantalla es decoración" al costo de una legibilidad ligeramente menor durante el Clímax de Combate, cuando el juego pide más al jugador. Mitigación planeada: la información crítica (vida baja, cooldown de reliquia disponible, selección activa) debe reforzarse con redundancia no visual (audio distintivo, feedback háptico si la plataforma lo soporta) para que la lectura no dependa únicamente de descifrar ornamento bajo presión — esta redundancia se especificará con más detalle en `/ux-design` y con el `sound-designer`. + +Los marcos, bordes e iconos siguen siendo angulares y de vitral emplomado (ver 3.2), y el color dorado/luminoso de una reliquia (Principio A) se extiende naturalmente al marco de su habilidad correspondiente en la UI — hasta el cooldown pip mismo. + +### 3.4 Formas de Héroe vs. Formas de Soporte — Qué Gana el Ojo Primero + +En un campo de batalla RTS abarrotado, la jerarquía visual debe imponerse por reglas de forma que funcionen incluso sin color ni animación (accesibilidad daltónica, legibilidad a escala de captura, condiciones de pantalla de gama baja). + +**La regla, en orden de prioridad para el ojo:** +1. **El kaiju primero, siempre** — la mayor masa contigua, la única forma horizontal-dominante en pantalla, la única forma que rompe la grilla angular del entorno en vez de pertenecer a ella. Nada — ni UI, ni efecto, ni floritura de héroe — puede escalarse o posicionarse para rivalizar visualmente con la masa del kaiju (Principio B, regla dura). +2. **El héroe segundo** — gana sobre las tropas ordinarias por ser la única anomalía en un campo vertical uniforme (figura-fondo), y gana atención independientemente del kaiju porque ocupa la banda de lectura "escala de unidad" que el kaiju ha desocupado al ser escala-arquitectura. +3. **Las tropas retroceden como textura, no individuos** — esto es la meta deliberada, no un fallo. Una masa de astillas verticales idénticas debe leerse como una multitud o un bosque: un campo texturizado que el ojo recorre en vez de detenerse en él. +4. **El entorno retrocede al final** — el detalle angular gótico permanece presente (nada es decoración) pero se mantiene de menor contraste y más regular/repetitivo que cualquier capa de unidades. + +**Lectura emocional de la jerarquía:** el ojo del jugador siempre es informado, en orden: "aquí está lo que no puedes vencer de frente, aquí está la persona que arriesgas para pelearlo de todos modos, aquí está la masa prescindible a su alrededor, aquí está el mundo reverente e indiferente en el que todo esto ocurre." Ese orden ES la tesis del juego convertida en patrón de escaneo visual. + +--- + +## 4. Color System + +### 4.1 Paleta Primaria + +Siete colores. Seis son el "default apagado" — el lenguaje visual de un mundo que aún no ha sido pagado. Uno es dorado, y no es tanto un color como un recibo. + +| Nombre | Hex | Significado en este mundo | +|---|---|---| +| **Piedra Vantablack** | `#1C1A1F` | La sombra base del fresco — oscuridad de catedral, roca tallada, las porciones sin iluminar de cada escena. | +| **Ocre de Tumba** | `#4A3B32` | Tierra, madera, cuero, el entorno construido de la civilización — cómo se ve un pueblo cuando nadie ha muerto todavía por él. | +| **Sangre Vieja** | `#7A1F2B` | Rojo desaturado y de aspecto seco — nunca rojo bermellón. Costo ya pagado, no peligro señalado (el peligro tiene su propio color, ver 4.2). Marca heridas, iconografía de sacrificio, el estado "a punto de gastarse". | +| **Hueso** | `#D8CFC0` | Blanco cálido desaturado. El pueblo del jugador — carne, pergamino, estandartes, retratos de héroe *antes* de morir. Es el polo opuesto de Dorado ("todavía vivo" vs "murió y se volvió eterno"). | +| **Acero Violeta-Ceniza** | `#3E4E5E` | Azul-violeta-gris frío. Noche, duelo, distancia, la presión atmosférica del kaiju sobre una escena. El color de "el mundo se enfría" — uso ambiental, no en personajes. | +| **Piedra Fría** | `#8C8478` | Gris medio con matiz cálido. Arquitectura neutral, terreno sin reclamar, material base del salón conmemorativo — el "lienzo" sobre el que se ganan los otros seis colores. | +| **Oro Reliquia** | `#F4C542` núcleo / `#FFE9A8` resplandor | El ÚNICO color luminoso y saturado del juego. Reservado exclusivamente (Principio A) para: reliquias, bendiciones, la luz puntual del momento de muerte del héroe, retratos del salón conmemorativo, la luz de amanecer de una era ganada. Nunca aparece como decoración, relleno ambiental o "buen resalte" — cada píxel de dorado es una afirmación: *un héroe murió para que esto exista.* | + +**Lógica de diseño**: seis colores forman una familia de baja saturación intencional. El dorado es deliberadamente el único color con alta saturación y un valor de resplandor/bloom acompañante, para que nunca pueda confundirse con nada más, incluso en una captura comprimida o una simulación de daltonismo. + +### 4.2 Uso Semántico del Color + +- **Oro Reliquia = Ganado / Permanente / Legendario.** El único color mecánicamente cargado del juego. Nunca significa "aliado", "bueno" o "adelante" de forma genérica — significa específicamente *esto existe porque un héroe murió.* +- **Sangre Vieja = Costo pagándose, ahora mismo.** Distinto del "costo ya pagado, inmortalizado" del dorado. Es el color del sacrificio activo — una barra de vida drenándose, la herida de un héroe. Se siente verídico en este mundo porque los óxidos de hierro rojos se usaban literalmente en vitrales y frescos religiosos — se siente antiguo y reverente, no "rojo de videojuego". Desaturado deliberadamente para que nunca compita con el Dorado por la atención. +- **Acero Violeta-Ceniza = Amenaza / Presencia del Kaiju / Pavor.** Frío, que retrocede, atmosférico. Según el Principio B, el kaiju no necesita un color ruidoso para leerse como peligroso — necesita *enfriar* visualmente la escena a su alrededor. Se usa como lavado ambiental que se propaga desde el kaiju hacia afuera. +- **Hueso = Aliado del jugador, con vida.** Blanco/pergamino cálido desaturado. Es el estado de reposo de cada unidad, retrato y estandarte del jugador — cálido porque todavía es parte de "la historia en curso", aún no resuelto en costo (Sangre Vieja) o leyenda (Dorado). +- **Ocre de Tumba / Piedra Fría = Mundo neutral, terreno sin reclamar.** Sin carga informativa — son literalmente los colores de "todavía no ha pasado nada aquí". +- **Los esbirros invocados por el kaiju** heredan la familia Acero Violeta-Ceniza del kaiju (son su progenie, visual y narrativamente), pero empujada hacia un tono más frío, plano y de bordes duros — realces de quitina en un `#5A6B72` desaturado en vez del lavado suave y brillante del kaiju. Esto refuerza a través del color la agrupación por similitud (Gestalt): el jugador aprende que "azul-gris familia-kaiju = hostil", independientemente de la silueta — pero la forma (Sección 3) sigue siendo la lectura primaria; el color es secundario. +- **El blanco/hueso nunca aparece en el kaiju o sus esbirros** — reservar el blanco-cálido estrictamente para "aliado del jugador, con vida" mantiene el campo semántico limpio: neutral-cálido = nosotros, neutral-frío = ellos, rojo = costo, dorado = leyenda. + +### 4.3 Temperatura de Color por Era + +Según la regla de la Sección 3.2 ("el vocabulario cambia, la regla permanece constante"), cada era-civilización obtiene su propio *desplazamiento de temperatura de acento* sobre la misma estructura de seis-apagados-más-dorado — nunca una regla nueva, nunca tonos nuevos para Dorado o Sangre Vieja. + +**Mecanismo**: cada era tiene un único desplazamiento de temperatura de acento aplicado a los dos neutrales flexibles (Ocre de Tumba y Piedra Fría), mientras que Hueso, Sangre Vieja, Acero Violeta-Ceniza y Dorado permanecen fijos en hex a través de todas las eras. + +*Nota: los nombres/cantidad exactos de eras están pendientes de `/map-systems`; la tabla siguiente es estructuralmente ilustrativa y debe poblarse con nombres reales de era en una pasada posterior.* + +| Slot de era (ilustrativo) | Desplazamiento Ocre/Piedra | Sensación de "capítulo" | Constante | +|---|---|---|---| +| Era 1 — temprana/agraria | Ocre hacia naranja-cálido (`#5C4530`) | Curtido por el sol, arcilla, alba de la civilización | Hueso, Sangre Vieja, Acero, Dorado fijos | +| Era 2 — clásica/piedra | Piedra hacia mármol frío (`#A8A199`) | Templo, mármol, formal | igual | +| Era 3 — medieval/hierro | Ocre hacia hollín (`#3A362E`) | Oscuridad de forja, industrioso, sombrío | igual | +| Era 4 — imperial/tardía | Piedra hacia pizarra-azulada fría (`#7C848C`) | Grandeza menguante, imperio sobre-extendido | igual | +| Era 5 (si se usa) — final/apocalíptica | Ocre desaturado casi-negro (`#241F1C`) | Al mundo casi no le queda nada que dar | igual | + +La **Transición de Era** (Sección 2.5) se ejecuta como un cross-fade específicamente entre los valores de acento Ocre/Piedra de cada era, mientras Hueso/Sangre Vieja/Acero/Dorado se mantienen constantes durante la mezcla — así la transición nunca implica accidentalmente que el color de muerte de un héroe o una reliquia cambió de significado. + +### 4.4 Paleta de UI + +Según la Sección 3.3, no existe una capa de HUD neutral separada — la UI se dibuja de esta misma paleta de siete colores, sin colores adicionales exclusivos de UI. + +- **Barras de vida**: se renderizan en **Hueso** (llena) drenando hacia **Sangre Vieja** (vacía) — no verde-a-rojo. Reutiliza directamente el vocabulario existente de 4.2. +- **Estado de "habilidad lista" (decisión: Opción A, estricta)**: solo las habilidades derivadas de reliquias/bendiciones (objetos literalmente derivados de una muerte, según la Sección 3.2) reciben el tratamiento de borde dorado al estar listas. Las habilidades base, no derivadas de reliquia, usan en cambio un borde-luz **Hueso** al estar listas. Esto preserva el Principio A sin excepciones — "listo para usar" no es en sí mismo un evento derivado de permadeath, así que no gana dorado solo por conveniencia de legibilidad. +- **Contorno de selección** (unidad/héroe seleccionado actualmente): **Acero Violeta-Ceniza** en alto valor/baja saturación — frío, discreto, informativo, nunca compitiendo con Dorado o Sangre Vieja por la atención. +- **Retículas de objetivo sobre esbirros/kaiju**: usan el mismo azul-gris frío de familia-esbirro de 4.2 (`#5A6B72`), reforzando la lectura hostil basada en forma en vez de introducir un "rojo hostil" exclusivo de UI. +- **Paneles de reliquias/Salón Conmemorativo**: el único lugar donde el Dorado se usa a saturación completa en la interfaz, porque literalmente están mostrando objetos derivados de una muerte — no es una excepción, es la regla aplicada correctamente. + +Resultado neto: toda la "máquina de estados" de la UI (vida llena → baja → muerta; cooldown → listo; sin seleccionar → seleccionado; reliquia vs. no-reliquia) se expresa mediante las mismas siete muestras usadas en el mundo, con las mismas asignaciones semánticas de 4.2. + +### 4.5 Seguridad para Daltonismo + +Dado que el Principio A codifica dorado-vs-todo-lo-demás como mecánicamente relevante (estado de reliquia/legendario), la señalización solo-por-color cerca de esa distinción es un fallo de accesibilidad real, no solo una preferencia. + +| Par de riesgo | Modo de fallo daltónico | Respaldo sin color | +|---|---|---| +| **Oro Reliquia vs. neutrales cálidos Hueso/Ocre** | Protanopía/deuteranopía pueden aplanar el dorado y los neutrales cálidos hacia tonos medios similares, especialmente a baja saturación en pantalla comprimida. Es el par de mayor riesgo de todo el sistema. | El Dorado siempre viene con (1) un bloom/pulso animado (respiración de brillo suave, ciclo ~1s) que ningún color apagado estático tiene, (2) un VFX de partículas/motas distintivo, y (3) un cambio de forma en el marco del ícono de UI (borde de rosetón ornamentado) en cualquier ítem marcado como dorado vs. un borde plano en ítems no-reliquia. El Dorado nunca se identifica solo por matiz. | +| **Sangre Vieja (rojo) vs. Acero Violeta-Ceniza (azul-violeta)** — "costo/vida" vs. "amenaza/kaiju" | Tritanopía puede comprimir el violeta hacia gris/rosa, arriesgando confusión con el rojo seco de Sangre Vieja. | Ocupan rangos de valor claramente separados y se usan en ubicaciones de UI distintas (Sangre Vieja solo dentro de barras de vida/costo; Acero solo como anillos de selección o niebla ambiental del mundo) — la separación posicional/contextual evita que deban distinguirse lado a lado. | +| **Azul-gris frío familia-kaiju (esbirros/kaiju) vs. contorno de selección Acero del jugador** | Ambos viven deliberadamente en la misma familia azul-violeta (lógica "kin del kaiju" de 4.2) — riesgo autoinfligido al usar el mismo matiz para dos roles semánticos distintos. | La forma lleva la lectura primaria (Sección 3, lenguaje angular/quitinoso vs. silueta del jugador); los anillos de selección son solo contorno delgado (sin relleno, sin brillo), mientras el color de esbirro hostil aparece como relleno de cuerpo/material con realces de borde duro. | +| **Acento dorado de "habilidad lista" vs. UI base Hueso** | Deuteranopía puede apagar el borde-luz dorado hacia un neutral cálido similar al Hueso base a escala pequeña de ícono. | El estado "listo" además dispara una animación de marco distinta (barrido brillante único al quedar listo) y una señal de sonido — movimiento y audio, no solo color estático. | +| **Sangre Vieja (costo/peligro) vs. cualquier rojo saturado futuro para "crítico"** | Si un pase futuro de UI añade un rojo más brillante para énfasis "crítico" sobre Sangre Vieja, la protanopía podría colapsar "herido" y "crítico" en un solo estado ilegible. | Recomendación (a coordinar con technical-artist/UX, no decidida aquí): los estados críticos deben escalar mediante **valor/contraste y movimiento** (destello de barra, pulso de viñeta) en vez de introducir un segundo rojo — manteniendo a Sangre Vieja como el único rojo del sistema, igual que el Dorado es el único dorado. | + +**Regla permanente**: cualquier propuesta futura de UI/VFX que señalice estado solo por color, sin respaldo de forma/animación/posición, debe marcarse en revisión — es el mayor multiplicador de riesgo de daltonismo en una paleta tan deliberadamente angosta. + +## 5. Character Design Direction + +### 5.1 Arquetipo Visual por Tipo de Personaje + +**Héroes.** El diseño de armadura/vestimenta de un héroe se construye alrededor de una *ornamentación asimétrica concentrada en un solo elemento* — extendiendo directamente la regla de forma de la Sección 3. Ese único elemento (brazo con arma en alto, capa que arrastra, estandarte fuera de eje, reliquia en la espalda) es también el *único* lugar del sprite donde se permite concentrar detalle ornamental: filigrana, patrones grabados, dorado desgastado, un ícono pintado tipo fresco. El resto de la silueta se mantiene comparativamente plano — placas de armadura, túnicas o pieles que se leen como funcionales, no decorativas. Esta economía de diseño significa que la futura reliquia (Pilar 2) tiene un "ancla" preexistente en el cuerpo del personaje que el juego puede literalmente desprender y llevar a eras futuras como artefacto. La ornamentación del héroe vivo nunca es Oro Reliquia (usa acentos Hueso y Piedra Fría/Ocre) — así que cuando el dorado sí aparece en esa misma silueta (post-muerte, como prop de reliquia), el cambio se lee como un cambio de estado que el jugador ha sido entrenado a reconocer. + +**Tropas.** Las tropas están diseñadas para estar intencionalmente *sub-diseñadas* — silueta uniforme, color uniforme (Hueso, indiferenciado), sin ornamento individualizado, sin asimetría. Lenguaje de diseño de tipo "estandarizado": formas de yelmo repetidas, postura sin estandarte repetida, rostros oscurecidos o abstraídos. Esto no es pereza en la ficción — es el argumento visual del Pilar 1: las tropas se dibujan para verse gastables *porque el jugador nunca debe sentir la misma fricción al gastar una tropa que al perder un héroe.* + +**Kaiju.** Los kaijus se leen como **divino-corrompido** — no simplemente monstruosos, no simplemente mecánicos, sino algo que alguna vez fue sagrado y ha sido roto o retorcido. Lógica de materiales: las superficies del kaiju combinan materiales *arquitectónicos* (piedra, vidrio emplomado, metal votivo oxidado, yeso de fresco agrietado) fusionados con *corrupción orgánica* (fisuras que brillan, crecimientos que se ven como vitral tumoroso en vez de carne). Esto le da al lavado ambiental Acero Violeta-Ceniza (Sección 4) una justificación material — no es solo un color de contraluz, se lee como la luz filtrándose a través de vidrio-carne corrompido y agrietado. El kaiju nunca debe verse puramente bestial ni puramente mecánico — debe verse como un **ídolo roto que todavía se mueve**. + +**Esbirros.** Extienden la familia material del kaiju en miniatura pero aplanada y endurecida: misma familia de forma angular/quitinosa, pero usando la variante fría aplanada `#5A6B72` con bordes duros en vez del lavado suave y brillante del kaiju. Se leen como *fragmentos o astillas desprendidas del kaiju* — pequeños, quebradizos, desechables, sin ninguna de la ambigüedad "todavía sagrado" que carga el kaiju. Son texturalmente más simples a propósito: menos información de silueta que un héroe, para garantizar la separación a escala RTS. + +### 5.2 Reglas de Rasgo Distintivo + +- **Héroe vs. héroe**: diferenciar exclusivamente por *cuál es el elemento asimétrico y dónde se ubica*, nunca cambiando la gramática vertical uniforme base. Esto mantiene a cada héroe legible como "un héroe" (familia de silueta) mientras permanece individualmente identificable de un vistazo. +- **Héroe vs. tropa**: la presencia o ausencia del elemento roto/asimétrico es toda la regla (fijada en la Sección 3). Sin excepciones. +- **Aliado vs. enemigo (tropas/héroes vs. esbirros)**: dos canales redundantes — (1) familia de forma: vertical/erguida (aliado) vs. baja/angular (esbirro), y (2) familia de color: Hueso cálido (aliado) vs. variante fría aplanada de Acero Violeta-Ceniza (esbirro). La Sección 4 ya garantiza que el Hueso nunca toca al kaiju o los esbirros, así que es una separación dura, no una tendencia estilística. +- **Diseño de héroe como anticipo de reliquia (Pilar 2)**: el *tipo* de elemento asimétrico que porta un héroe debe insinuar visualmente la categoría de reliquia que su muerte eventualmente producirá — un héroe con brazo-arma-en-alto anticipa una reliquia ofensiva, un héroe con estandarte anticipa una reliquia de rally/buff, un héroe que ya entra en juego cargando una reliquia heredada en la espalda anticipa una reliquia defensiva o utilitaria ligada a ese objeto específico. El jugador lee potencial narrativo en la silueta de un héroe desde el momento del reclutamiento, no solo en el momento de su muerte. + +### 5.3 Objetivo de Estilo de Expresión/Pose + +**Objetivo: poses rígidas e icónicas, tipo estatuaria religiosa** — más cerca de figuras de fresco/ícono que de animación expresiva o realista (con una excepción deliberada y estrecha en los momentos de muerte). + +Justificación: la regla visual central de la Sección 1 enmarca cada escena como "un fresco de vitral en píxeles" — las figuras de fresco/ícono se definen por poses contenidas, simbólicas, a menudo frontales o de perfil, no por lenguaje corporal naturalista. Un estilo de pose expresivo/dinámico competiría con esa identidad y erosionaría la distinción héroe/tropa de 5.2, ya que la expresividad es en sí misma una forma de individualización que se les niega explícitamente a las tropas. + +**Excepción deliberada**: en el momento mismo de muerte/decisión, el vocabulario de pose puede romper su propia rigidez — la pose de muerte sostenida de un héroe es el único lugar donde se usa una silueta más dinámica y dramática (colapsando, alcanzando, cubriéndose), y solo ahí. Es la implementación directa del Principio C y el Pilar 4 — la pose debe cargar legibilidad emocional completa solo a través de la silueta, ya que no hay cutscene, ni primer plano de rostro, ni diálogo en el que apoyarse. Una figura estatuaria que de pronto rompe su iconografía para caer es en sí misma el beat dramático; la rareza de las poses expresivas es lo que le da peso a esa única pose. + +### 5.4 Filosofía de LOD + +**Regla: dos niveles fijos de detalle, controlados por distancia de cámara y relevancia narrativa, nunca un LOD continuo suave.** + +- **Nivel 1 — Escala de juego (default, muchas unidades en pantalla)**: los personajes se leen solo a resolución de silueta-y-bloque-de-color. No se espera que sobreviva detalle facial ni ornamento fino a esta escala — por eso la Sección 5.1 concentra toda la ornamentación en el único elemento asimétrico: es la única parte de un héroe que *necesita* sobrevivir a la legibilidad de Nivel 1. +- **Nivel 2 — Detalle sostenido/de primer plano (reservado, activado solo por permadeath y momentos conmemorativos)**: detalle ornamental y material completo — el renderizado pictórico a nivel de fresco de la Sección 1 — reservado exclusivamente para: (a) el beat "sostenido" de muerte de héroe (Sección 2.3), donde la cámara sostiene la mirada y el juego puede cortar a un renderizado más grande y detallado de ese mismo sprite sin romper la regla "sin cutscenes" del Pilar 4, ya que es una pausa sobre la misma escena, no una cinemática separada; (b) retratos del salón conmemorativo, donde un héroe muerto se representa a detalle completo como registro permanente; (c) revelación de reliquia, donde el material dorado (Sección 4) obtiene su único momento de renderizado material completo. + +La regla de *cuándo* se activa el Nivel 2 es estricta y angosta por diseño: **solo el permadeath y sus artefactos directos posteriores (memorial, reliquia) ganan renderizado a detalle completo.** Nada más — ni muertes de tropa, ni acciones rutinarias de héroe, ni comportamiento idle del kaiju — puede activar el nivel de primer plano. Esta escasez es el mecanismo que mantiene al Nivel 2 significativo. + +## 6. Environment Design Language + +### 6.1 Estilo Arquitectónico y su Relación con la Cultura/Historia del Mundo + +La arquitectura de cada era-civilización es un registro físico de dos presiones superpuestas: **cuánto miedo tiene esta cultura de su kaiju** y **cuánto ya ha pagado por sobrevivir**. La gramática angular/vertical fijada en 3.2 es la constante; cómo una cultura *despliega* esa gramática cambia a lo largo de dos ejes: + +- **Eje de postura defensiva**: la arquitectura de era temprana (primeras confrontaciones) tiende hacia lo ritual y lo abierto — plazas, accesos procesionales, estructuras construidas para ser *vistas* desde lejos porque la amenaza aún es novedosa. Conforme una civilización acumula pérdidas, la arquitectura se vuelve visiblemente defensiva e introvertida — muros más gruesos, aberturas más angostas, accesos que se vuelven cuellos de botella — sin dejar de compartir el mismo ADN dentado/vertical. El arco apuntado que alguna vez fue entrada de templo se vuelve tronera. +- **Eje de densidad reliquiaria**: según el Pilar 2, cada muerte de héroe produce una reliquia legendaria, y según el mood del Salón Conmemorativo (Sección 2), estas se acumulan como objetos iluminados literales con nichos vacíos y sin luz para lo aún no ganado. Arquitectónicamente, esto significa que el entorno construido de una cultura debe mostrar *infraestructura reliquiaria creciente* cuanto más tiempo ha sobrevivido: los edificios de era temprana tienen pocos o ningún nicho dedicado (nada que consagrar todavía); los edificios de era tardía — incluso los puramente funcionales como cuarteles o puertas — brotan nichos votivos, pedestales y dinteles inscritos, porque la cultura ha aprendido que el sacrificio necesita un lugar adónde ir. + +Combinado, esto da un arco emocional sin palabras por era: **abierto y ritual → fortificado y reliquiario → (en las eras tardías más desesperadas) improvisado y canibalizado**, donde la construcción nueva reutiliza visiblemente fragmentos angulares de eras anteriores como material estructural en vez de ornamento (una losa megalítica de la Edad de Bronce cargando peso dentro de un muro fracturado de varilla). Esta es la expresión arquitectónica directa de la regla de persistencia de reliquias de 3.2: no basta con que los props viejos estén *cerca* de los edificios nuevos — en las eras más desesperadas, los props viejos deben estar *construidos dentro* de los edificios nuevos, porque el Pilar 2 dice que el pasado se vuelve poder, y aquí el pasado literalmente se vuelve soporte estructural. + +### 6.2 Filosofía de Textura + +**Decisión: trabajo de textura pixel pintado y de autoría manual, con iluminación pictórica direccional horneada en el arte** — no un enfoque plano/estilizado de color plano, y no un enfoque simulado tipo PBR. + +Justificación: la regla visual central de la Sección 1 — "cada escena se lee como un fresco de vitral en píxeles" — es una metáfora *pintada*, no de simulación de material. El vitral y el fresco derivan su impacto de gradientes de luz y color colocados a mano, no de rugosidad de microsuperficie simulada. Un pipeline pixel tipo PBR (tiles con normal maps, resaltos especulares simulados) competiría con esta identidad haciendo que las superficies se lean como "renderizadas" en vez de "pintadas". Blasphemous 2 es la referencia explícita de calidad de iluminación pictórica: sus entornos se leen como si cada tile hubiera sido iluminado a mano por un artista decidiendo dónde debe caer el dorado, la sombra y el color-sangre, independientemente de una fuente de luz físicamente consistente. + +Prácticamente: las texturas base son pixel art de sombreado plano con **oclusión ambiental horneada y resalte/sombra direccional pintado a mano** dentro del propio arte del tile, coincidiendo con el mood-state dominante de la Sección 2. Las luces dinámicas 2D de Godot (CanvasModulate/Light2D) se usan con moderación encima para las transiciones de mood-state, no para simular respuesta física de superficie. Esto también refuerza el sistema de neutrales flexibles de la Sección 4: como Ocre de Tumba y Piedra Fría son tonos base *pintados* que cambian por era, un pipeline pintado permite que el artista de cada era repinte directamente los neutrales base del mismo tileset arquitectónico, sin necesitar reautoría de material PBR por era. + +### 6.3 Reglas de Densidad de Props + +El conflicto rector: la regla "nada en pantalla es decoración" de la Sección 1 exige ornamentación narrativamente cargada, mientras que la legibilidad de unidades RTS exige líneas de visión limpias durante el combate. La reconciliación: **la densidad es una función del mood-state, no del tipo de área** — se *gana* en estados lentos y se *suprime* en estados rápidos, usando el mismo set de assets en ambos casos. + +- **Preparación (votivo, medido)**: densidad de props máxima permitida. Es el estado donde el jugador tiene tiempo de mirar, así que aquí se adelanta el detalle reliquiario, el desorden conmemorativo y el ornamento de vocabulario de era. +- **Clímax de Combate (alto contraste)**: la densidad está mecánicamente limitada. Al entrar a este mood-state, cualquier capa de props de primer plano no crítica para la silueta se elimina, se empuja a solo-fondo, o se convierte en un elemento de telón aplanado y de bajo contraste. Regla general: **si un prop no puede distinguirse de un sprite de unidad a distancia de cámara de juego en menos de un vistazo, no pertenece al mismo plano visual que las unidades durante el Clímax.** La corrupción geométrica del territorio del kaiju (Sección 3.2) es la única texturización densa de primer plano permitida durante el combate, ya que se lee como información de terreno (Pilar 4), no como desorden. +- **Muerte/Sacrificio y Salón Conmemorativo (luz única/solo-reliquia)**: densidad deliberadamente escasa y muy compuesta — pocos objetos, cada uno significativo, ya que la cámara sostiene la mirada en este momento (Principio C) y lo que esté en pantalla debe recompensar esa atención sostenida. +- **Transición/Victoria de Era**: sigue las reglas de Preparación, pero la composición debe adelantar props que señalen específicamente el cambio de vocabulario de era (6.1). + +### 6.4 Lineamientos de Narrativa Ambiental + +Dado que el Pilar 4 descarta cutscenes/texto, cada detalle visual recurrente listado abajo es un *vocabulario* que el jugador debe poder aprender a leer en todo el juego: + +- **La colocación de reliquias comunica jerarquía de pérdida.** Un prop de reliquia (Pilar 2) nunca debe colocarse neutralmente en una escena — su posición física comunica si esta muerte es honrada (elevada, iluminada, central en la composición) o escondida/sin resolver (baja, semi-sombreada, fuera de la línea de visión principal). Los nichos vacíos y sin luz (ya fijados en la Sección 2) son el vocabulario visual del juego para "un héroe que el jugador aún no ha perdido" — el *conteo* de nichos en un Salón Conmemorativo dado es en sí un indicador (sin spoilers) de cuántas pérdidas mayores espera la estructura narrativa de esta era, y debe tratarse como un número de diseño narrativo/estructural, no una decisión de arte arbitraria. +- **La cicatrización del kaiju es un gradiente de legibilidad, no un estado binario de daño.** Según 3.2, la corrupción debe escalar con la *proximidad narrativa* al kaiju, no solo la distancia literal — un pueblo por el que el kaiju pasó una vez debe mostrar cicatrización más leve y en sanación (parches de musgo/reconstrucción sobre el tajo), mientras que su coto de caza actual debe mostrar corrupción cruda y sin sanar. Esto convierte la cicatrización del kaiju en una línea de tiempo legible de su recorrido por esta era, sin necesitar UI de mapa. +- **La persistencia de props entre eras es el dispositivo principal de "cómo llegamos aquí".** Según 3.2 y el Pilar 2, las reliquias/props de eras anteriores sentados dentro de un entorno de era nueva son el hilo histórico central sin palabras del juego. Lineamiento de *dónde* colocarlos: los props de era anterior deben aparecer con mayor densidad en las estructuras más defendibles y reliquiarias de la era nueva (6.1) — porque ahí es temáticamente donde una cultura habría cargado físicamente los objetos sobrevivientes más importantes de sus predecesores. +- **La construcción improvisada/canibalizada (eje tardío de 6.1) es en sí misma narrativa ambiental sobre la desesperación**, y debe reservarse — no usarse ampliamente — para que cuando aparezca, se lea como una señal de que esta civilización específica está en crisis, no simplemente "vieja". +- **Regla de densidad-como-narración**: dado que 6.3 ata la densidad de props al mood-state y no a la ubicación, un lugar que inesperadamente *rompe* su patrón de densidad esperado es en sí mismo un dispositivo narrativo — por ejemplo, un espacio de Preparación despojado de forma inusualmente escasa le dice al jugador "algo fue tomado/perdido aquí" antes que cualquier otra señal. Debe usarse deliberada y raramente, como revelación. + +## 7. UI/HUD Visual Direction + +### 7.0 Restricción Rectora + +Toda la UI es completamente diegética según la Sección 3.3 — no existe una capa de HUD neutral/estéril. Cada regla abajo existe para recuperar legibilidad perdida por esa decisión, principalmente a través de disciplina tipográfica y claridad de silueta de íconos, sin romper la identidad de fresco/vitral de la Sección 1. + +### 7.1 Tipografía + +**Sistema tipográfico de dos pistas:** + +| Pista | Uso | Personalidad | Reglas de peso/tamaño | +|---|---|---|---| +| **Tallada (Display)** | Nombres de reliquias, títulos de habilidad, encabezados de panel, inscripciones del Salón Conmemorativo | Serif con acentos góticos — capitales talladas y angulares, floritura restringida solo a mayúsculas, nunca en trazos de cuerpo de texto en minúscula | Encabezados: grandes, alto peso, espaciado generoso (se lee como "cincelado en piedra/vidrio"). Nunca por debajo de ~18px equivalente — el detalle gótico colapsa a tamaños pequeños | +| **Clara (Numeric)** | Números de vida, temporizadores de cooldown, números de daño, contadores de recursos, duraciones de estado | Serif restringido tipo slab/humanista, formas numéricas casi geométricas, ornamento mínimo | Alto contraste, numerales tabulares (monoespaciados) para que los valores no salten de ancho al cambiar. Es el único lugar donde se prioriza la legibilidad pura sobre el ornamento — no negociable dado el tradeoff aceptado en 3.3 | + +**Jerarquía (de mayor a menor):** 1) Nombres de reliquia/memorial (pista Tallada), 2) Encabezados de sección de panel (Tallada, tamaño reducido), 3) Numéricos de cooldown/vida/recursos (pista Clara) — deben permanecer legibles a velocidad de combate, 4) Texto de cuerpo/lore (Clara, peso más ligero), 5) Micro-etiquetas (nombres de efectos de estado, tags de esbirros) — Clara, tamaño más pequeño, siempre emparejadas con un ícono, nunca solo texto. + +**Regla:** si un diseño requiere tipografía gótica Tallada en algo que el jugador deba leer *durante* el Clímax de Combate (temporizadores, vida, cooldowns activos), es una violación — se redirige a la pista Clara. + +### 7.2 Estilo de Iconografía + +**Estilo base: plano, basado en silueta primero, con relleno de un solo color y una pasada de detalle ornamental encima.** No pictórico, no fotorrealista, no solo-contorno (un contorno puro pierde velocidad de lectura a escala de UI). + +- **Íconos de habilidad base (no-reliquia):** silueta plana en tonos neutrales de la paleta (familia Hueso/Acero Violeta-Ceniza). Construcción puramente gráfica — sin hoja de oro, sin detalle de iluminación. Es la línea base contra la que se mide todo lo demás. +- **Íconos de habilidad derivada de reliquia:** misma base de silueta plana, pero con una pasada añadida de detalle tipo manuscrito iluminado — líneas finas de plomo dorado integradas en el interior de la silueta (no solo un efecto de borde, una diferencia de construcción real). Esto hace que los íconos de reliquia sean reconocibles incluso en silueta/condiciones de daltonismo/poca luz, independientemente del borde dorado de estado-listo (Sección 4.4). +- **Íconos de panel de reliquia (Memorial/inventario, tamaño completo):** la única categoría de ícono con tratamiento completo de vitral — contorno emplomado, dorado saturado, pequeña ilustración interna tipo fresco. Se miran durante la Preparación (ritmo lento), así que el costo de ornamento completo es aceptable aquí. +- **Íconos de estado/debuff:** solo silueta plana, sin ornamento, el nivel de mayor contraste de todo el sistema de íconos (deben leerse en menos de un segundo durante el Clímax). Codificados por color según las asignaciones semánticas del sistema de 7 colores — nunca dorado, ya que el dorado se reserva exclusivamente (Principio A) y los debuffs nunca son "ganados". + +### 7.3 Sensación de Animación de UI + +**Principio central: los elementos de UI se comportan como objetos físicos de vidrio emplomado/madera-y-metal, nunca como software.** + +- **Apertura/cierre de paneles:** movimiento de bisagra-giro o despliegue (como si un panel emplomado o puerta de santuario rotara físicamente), nunca un fundido o deslizamiento digital. Duración: sin prisa durante la Preparación (400-600ms), puede comprimirse algo durante el Clímax para paneles que deban aparecer en combate, pero nunca por debajo de un piso que se lea como "instantáneo/snap". +- **Anuncio de reliquia lista:** una "ignición" de vitral — líneas de plomo dorado se iluminan en secuencia (como si la luz atravesara el vidrio), culminando en el estado de borde dorado completo (Sección 4.4). Es un beat, no un salto de estado — debe sentirse ceremonial. +- **Retroalimentación de daño/vida:** la barra de vida drena suavemente de Hueso a Sangre Vieja, sin easing elástico/rebotante — el rebote se lee como feedback de arcade, lo cual entra en conflicto con el tono reverente de la Sección 1. +- **UI del momento de muerte sostenido (vínculo con la Sección 2.3):** durante el beat sostenido, **toda la UI se congela** — ninguna barra anima, ningún ícono transiciona, ningún numérico cuenta regresivamente. Regla dura, no sugerencia: la quietud de la UI es lo que extiende el Principio C a la capa de interfaz. Solo cuando el beat sostenido se libera visualmente, la UI reanuda — la vida drena a cero real, los íconos de habilidad del héroe muerto se atenúan a gris, y (si aplica) el ícono de la nueva reliquia-en-formación comienza su primer indicio de ignición. + +### 7.4 Reglas de Ornamentación de Marco + +La elaboración del marco está atada directamente al estatus de "ganado", extendiendo el Principio A a una escala completa de ornamentación: + +| Nivel | Elemento | Tratamiento de Marco | ¿Dorado? | +|---|---|---|---| +| **0** | Retícula de objetivo enemigo/esbirro | Sin marco — solo la forma de marcador azul-gris frío (`#5A6B72`) | Nunca | +| **1** | Ícono de habilidad base | Marco rectangular/hexagonal emplomado simple de una línea, borde-luz Hueso en estado listo | Nunca | +| **2** | Ícono de habilidad derivada de reliquia | Marco emplomado + detalle de tracería en esquinas; gana borde dorado completo solo cuando la habilidad está lista (Opción A, Sección 4.4) | Solo en estado-listo | +| **3** | Panel de Reliquia/Memorial (vista completa de inventario/santuario) | Marco ornamental completo — tracería en los cuatro bordes, dorado saturado en todo, ilustración de esquina tipo fresco | Siempre, completamente saturado | + +**Regla rectora:** la complejidad de marco de un elemento nunca puede exceder lo que su nivel permite, sin importar cuán "importante" parezca en aislamiento — esto es lo que mantiene al dorado y al ornamento significativos en vez de inflacionarios. Cualquier elemento de UI futuro debe encajar en uno de estos cuatro niveles antes de producirse — no existe un quinto nivel ad hoc. + +### 7.5 Chequeo de Alineación UX y Mitigaciones de Legibilidad + +El `ux-designer` identificó que, dado que el Pilar 1 (Sacrificio con Peso) depende de que el jugador tome una decisión *legible y con peso* sobre quién vive o muere, la legibilidad reducida de una UI diegética no es solo cosmética — si el jugador no puede leer confiablemente el estado de un héroe, la "elección" se degrada a una adivinanza, lo cual socava el pilar aunque el resto de sistemas siga funcionando. Mitigaciones acordadas, todas preservando la pureza diegética (canales de apoyo no visuales o supplementarios, no una segunda capa de UI): + +- **Lectura de vida multi-héroe en el Clímax (mayor riesgo):** cambios de estado por umbral (50%/25%/crítico) disparan un evento diegético animado distintivo (grietas extendiéndose por el marco, una vela/ascua apagándose) en vez de depender de que el jugador juzgue un gradiente continuo bajo presión. Se refuerza con una escalera de audio por héroe (timbre/tono distintivo por umbral) y un pulso háptico distinto al cruzar "crítico" en consola. +- **Legibilidad de cooldown de habilidad:** un timbre/sonido distintivo y raro reservado solo para cuando una habilidad de reliquia queda lista; las habilidades base usan una señal más discreta — esto también refuerza la jerarquía de rareza del dorado a través del sonido, no solo lo visual. +- **Claridad de selección:** el contorno Acero Violeta-Ceniza se empareja con una animación de pulso/respiración sutil (no depende solo de discriminación de matiz) y un chime de audio suave al cambiar de selección. +- **Targeting sin cursor en gamepad:** cuando se detecta entrada de gamepad, el marco diegético de la unidad seleccionada reproduce una versión *más intensa* de la misma animación de selección (mayor amplitud de pulso), más un tick de audio espacializado panorámico hacia la posición en pantalla de la unidad recién seleccionada — mismo lenguaje visual, amplificado, no una señal nueva. *(Depende del sistema de targeting/ciclo de gamepad aún por definir en otra parte del proyecto — revisar esta mitigación una vez que ese sistema se decida.)* +- **Excepción de accesibilidad (decisión confirmada):** se incluye un **toggle de accesibilidad opt-in, apagado por defecto** que ofrece mayor contraste en las señales de vida/selección para quien lo active — esta es la única excepción reconocida a la pureza diegética total, y aplica solo a quien lo habilita explícitamente, sin afectar la experiencia diseñada por defecto para el resto de jugadores. El detalle completo de esta especificación se desarrollará en `/ux-design` bajo `design/ux/accessibility-requirements.md`. + +## 8. Asset Standards + +### 8.1 Formatos de Archivo + +- **Fuente (autoría)**: Aseprite (`.ase`/`.aseprite`) como archivo canónico para todo el trabajo pixel pintado a mano — héroes, tropas, kaiju, esbirros, tiles, íconos de UI, props de reliquia. Aseprite preserva disciplina de paleta indexada, crítico para la adherencia estricta al sistema de 7 colores (Sección 4). Los grupos de capas por nivel de LOD y por frame de animación deben vivir en el mismo archivo fuente. +- **Exportación**: PNG, 32-bit RGBA, sin artefactos de compresión con pérdida, alfa no premultiplicado, etiquetado sRGB para la gestión de color por defecto de Godot 4.6. Ningún JPG en ningún punto del pipeline. +- **Sprite sheets vs. frames individuales**: preferencia por atlases de textura organizados por categoría de asset (no por entidad individual) — mejora el batching de draw calls (Sección 8.2) y facilita auditar de un vistazo la regla constante de era (Sección 6.1). **Excepción**: los assets de Nivel 2 de LOD (permadeath/memorial/revelación de reliquia) se exportan como PNGs individuales de alta resolución, fuera del atlas de juego — son raros, grandes, y empaquetarlos junto al atlas de Nivel 1 inflaría la memoria residente del 99% del tiempo. + +### 8.2 Convención de Nomenclatura + +Extendiendo el patrón estándar del proyecto `[categoría]_[nombre]_[variante]_[tamaño].[ext]`: + +| Prefijo de categoría | Aplica a | Ejemplo | +|---|---|---| +| `hero_` | Sprites de héroe | `hero_ashveil_idle_lod1_small.png` | +| `troop_` | Sprites de tropa | `troop_spear_walk_lod1_small.png` | +| `kaiju_` | Sprites de kaiju | `kaiju_maw-of-the-cathedral_attack_lod1_large.png` | +| `minion_` | Sprites de esbirro | `minion_chitin-shard_idle_lod1_small.png` | +| `env_[era]_` | Tiles de entorno por era | `env_bronze_wall-fractured_variant02_medium.png` | +| `ui_icon_[tier]_` | Íconos de UI por nivel (0-3, Sección 7.4) | `ui_icon_tier2_relic-frame_ready_small.png` | +| `relic_` | Props de reliquia | `relic_ashveil-standard_display_large.png` | + +Reglas de variante adicionales: +- **LOD es un slot de variante, no de tamaño** (`lod1`/`lod2` explícito en el nombre) — hace imposible enrutar accidentalmente un render de Nivel 2 a un atlas de juego. +- **La era es un modificador de categoría, no una variante**, para tiles de entorno (`env_[era]_...`) — facilita auditorías de arte y QA de cambio de paleta por era (Sección 4.3) por grep. +- **Los nombres de héroe usan su nombre/epíteto propio, no un ID numérico** (`hero_ashveil_...`) — facilita referencia cruzada de la cadena de assets reliquia/memorial de un héroe sin tabla de búsqueda. +- **El número de nivel de ícono de UI en el prefijo es obligatorio**, coincidiendo exactamente con el sistema de 4 niveles de la Sección 7.4 — permite hacer grep de "todo asset autorizado a usar Oro Reliquia saturado" (solo tier2/tier3). + +### 8.3 Niveles de Resolución de Textura (Reconciliado) + +| Categoría | Resolución nativa | Frames por estado de animación | Notas de reconciliación | +|---|---|---|---| +| **Tropa** | 32×32 a 48×48px | 4-6 idle/walk, 3-4 ataque | Ideal de arte (24-32px) y techo técnico (≤48×48px, ≤6 frames) compatibles sin conflicto — categoría crítica de batching (empaquetada en atlas compartido por era) | +| **Héroe (Nivel 1, escala de juego)** | 48×64 a 96×96px | 6-8 idle/walk, 6-10 ataque/habilidad | Ideal de arte y techo técnico (≤96×96px, ≤10 frames) compatibles; roster pequeño permite el extremo superior del rango | +| **Héroe (Nivel 2, sostenido/portrait)** | Hasta 512×512px o mayor en still/secuencia corta (128-256px de alto como referencia de arte) | Secuencia cinemática corta, no animación de loop | Sin techo estricto del presupuesto de combate — pase aislado, nunca concurrente con renderizado de combate | +| **Kaiju** | 256-512px en su eje largo, segmentado en partes/miembros compartiendo un atlas | Set de fase de ataque completo; sin set de idle más allá de la silueta de horizonte de Preparación | Sin techo estricto por frame, pero footprint total de VRAM no debe exceder ~1/4 del presupuesto de trabajo de combate (~64-96MB) — segmentar en sub-sprites que compartan atlas, no un monolito | +| **Esbirro** | 20-28px (ideal) hasta 64×64px (techo) | 4-6 frames | Empaquetado en atlas compartido por kaiju de origen | +| **Tile de entorno (por era)** | Grid de tile único y fijo en las 4-5 eras: **48×48px (confirmado)** — decidido en `design/gdd/datos-de-era-civilizacion.md` (`world_unit_scale`) por consistencia de grid con tropas (32-48px) y héroes (48-96px) | Estático, 2-4 frames en loops ambientales ocasionales (antorchas, brillo de vitral) | Un tamaño de grid único en todo el proyecto es lo que permite que el motivo "reliquias de era vieja construidas en muros de era nueva" (Sección 6.1) funcione visualmente | +| **Ícono de UI Nivel 0-2** (concurrentes con combate) | Nivel 0: 16-20px / Nivel 1: 32px / Nivel 2: 32px con linework interno más fino | Estático | Techo técnico ≤64px respetado — concurrentes con el renderizado de combate | +| **Ícono de UI Nivel 3** (paneles Memorial/reliquia) | 64-96px (excepción concedida) | Estático | **Reconciliación explícita**: excepción al techo genérico de ≤64px, justificada porque los paneles de Nivel 3 se ven durante estados lentos (Preparación/Panteón) sin concurrencia con el conteo de draw calls del Clímax — mismo argumento que exime al Nivel 2 de héroe | + +### 8.4 Expectativas de Nivel de LOD (Términos de Producción) + +**Nivel 1 (escala de juego, requerido para todo tipo de personaje, sin excepciones):** +- Estados de animación idle, movimiento, ataque, reacción a golpe, muerte (tropa/esbirro) o pose-caído (héroe), cada uno como secuencia de frames. +- Los héroes necesitan además el único elemento de ornamento asimétrico (Sección 5.1) autorado una vez y reutilizado/rotado en todos los estados de animación de Nivel 1 — no debe redibujarse por estado. +- El kaiju de Nivel 1 necesita su set completo de fase de ataque, pero no necesita un set de idle más allá de la silueta visible durante el arrastre de horizonte de Preparación. +- Esbirros: solo Nivel 1, siempre — son fragmentos desechables del kaiju por diseño (Sección 5.1), sin momento de detalle narrativo reservado. + +**Nivel 2 (reservado, alto detalle, activado solo narrativamente) — exactamente 3 assets por héroe, para todo el juego:** +1. Una pose de muerte sostenida por héroe (Sección 2.3/5.3). +2. Un retrato de salón conmemorativo por héroe (Sección 2.4). +3. Un render de revelación de reliquia por el prop resultante de cada héroe (Sección 4.1/7.2). + +Sin assets de Nivel 2 para tropas, esbirros, o estados de idle/ataque del kaiju. *(Un posible "golpe final" de Nivel 2 para el kaiju queda como pregunta abierta para una futura pasada de diseño, no asumido aquí.)* Esta escasez es el punto — si el conteo de Nivel 2 crece por héroe o se extiende a categorías no-héroe, la lectura de "el detalle se gana con permadeath" colapsa. + +### 8.5 Filosofía de Configuración de Exportación + +- **Sin filtrado, nunca, en ningún asset pixel 2D.** Todo `Texture2D` debe usar filtrado **Nearest** (override de proyecto, no por sprite) — nunca Linear/Bilinear/Trilinear. +- **Sin mipmaps** en sprites a escala pixel (héroes/tropas/esbirros/íconos de UI) — ahorra ~33% de memoria por textura sin pérdida de calidad a esta escala. *Excepción a discutir*: el kaiju y fondos de entorno grandes podrían beneficiarse de mip-blur a distancia por profundidad atmosférica — tratar como excepción explícita y revisada, no default. +- **Solo escalado entero** para el modo de stretch del viewport (`stretch/scale_mode` en escalado entero, no "expand"/"viewport" fraccional) — el escalado fraccional reintroduce el mismo problema de blur en la capa de despliegue incluso con texturas fuente perfectas, y es donde primero muere el linework fino de vitral (Secciones 6.2, 7.2). +- **Compresión de textura**: modo **Lossless** (sin pérdida) requerido sobre compresión VRAM con pérdida (S3TC/BCn) — el trabajo de color plano de 7 colores mantiene el footprint de textura naturalmente bajo incluso sin compresión, así que el costo de VRAM adicional por usar lossless es aceptable dado el presupuesto propuesto en 8.6. +- **Validación de paleta indexada/fija en exportación**: cada PNG exportado debe verificarse (script o manual) contra el sistema de 7 colores por era (5 hex fijos + 2 neutrales variables por era, Sección 4.3) antes de considerarse final — es el mecanismo real de enforcement del sistema de color. +- **Alfa recto no-premultiplicado** en todos los PNG transparentes, configuración de importación consistente en todo el proyecto. + +### 8.6 Restricciones Técnicas Duras (Godot 4.6) + +- **Presupuesto de VRAM propuesto** (pendiente de confirmación de producer/technical-director, ya que "Memory Ceiling" sigue `[TO BE CONFIGURED]` en `technical-preferences.md`): **256-384 MB de VRAM de textura para el set de trabajo activo en combate** (sprites de Nivel 1 + tileset de era activa + UI activa). Conservador para PC pero realista para certificación de consola. +- **Mecanismo de draw calls (conexión directa al presupuesto de <1000 draw calls)**: el renderizador 2D de Godot 4.6 agrupa comandos de dibujo consecutivos en una sola llamada de GPU **solo cuando comparten la misma textura, el mismo material, y se dibujan en la misma capa de canvas/orden-z sin un dibujo de textura distinta interrumpiendo entre medio.** Regla práctica: empaquetar todas las variantes de tropa de una era en un atlas compartido, todas las variantes de esbirro en un atlas compartido, íconos de UI en un atlas por pantalla/contexto de UI, y las sub-partes del kaiju en un atlas compartido entre sí. Esto convierte "cientos de tropas" en un puñado de draw calls en vez de cientos. Separar sprites de mundo y UI diegética en `CanvasLayer`s distintos para no romper el batching por intercalado de orden-z. +- **Presupuesto numérico**: de <1000 draw calls totales, reservando ~400-500 para UI/fondo/entorno/kaiju, quedan **~500-600 draw calls para todas las instancias de tropa/héroe/esbirro visibles simultáneamente** — suficiente *solo si* se respeta el atlas-batching de arriba. +- **Slots de material (Godot 4.6, 2D)**: cada `ShaderMaterial` único asignado a un `CanvasItem` es un rompedor de batching igual que un cambio de textura. Definir un set pequeño y fijo de materiales de shader compartidos por categoría (propuesta: ≤6-8 `ShaderMaterial` distintos activos simultáneamente en un frame de combate — tropa/esbirro estándar, héroe estándar, segmento de kaiju, brillo de reliquia/leyenda, ambiental de entorno, marco de ícono de UI, panel diegético de UI, material de Nivel 2 que nunca se solapa con combate). Evitar overrides de uniform por instancia en tropas/esbirros (fuerzan clonado de material y rompen el batching); reservar variación de shader por instancia para héroes y kaiju. +- **Restricciones de importador (Godot 4.6)**: dado que la versión del motor cae fuera de los datos de entrenamiento del modelo (ver `docs/engine-reference/godot/VERSION.md`, riesgo ALTO), lo siguiente debe **verificarse contra la documentación oficial de Godot 4.6 antes de fijarse como definitivo**: comportamiento del shader baker (marcado de riesgo alto en 4.5) sobre compilación/caché de shaders de atlas al importar — posible hitch de carga en shaders de brillo de reliquia/kaiju si no se pre-calientan; y cualquier suposición heredada de Godot pre-4.4 sobre comportamiento de batching de `AtlasTexture`/`SpriteFrames` o nombres de preset de compresión. + +**Nota abierta**: el presupuesto de VRAM (256-384 MB) es una propuesta técnica pendiente de confirmación formal — se recomienda actualizar el campo "Memory Ceiling" en `.claude/docs/technical-preferences.md` una vez aprobado. + +## 9. Reference Direction + +### 9.0 Cómo Usar Esta Sección + +Ninguna referencia listada aquí debe copiarse como estilo. Cada una aporta una **técnica específica** a un principio, sección o regla ya fijados en este documento — nunca "la sensación general" de la obra de referencia. Cada entrada incluye igualmente qué evitar explícitamente, para que ningún futuro pase de arte pueda justificarse solo con "así lo hace [referencia]" sin comprobar que también respeta el lado del "evitar". + +### 9.1 Blasphemous 2 (videojuego) + +**Extraer específicamente**: su disciplina de iluminación pictórica dentro de una grilla estricta de píxeles — cada tile parece iluminado a mano por un artista decidiendo dónde cae la luz, la sombra y el color-acento, en vez de derivar de una fuente de luz físicamente simulada. Referencia directa de **Principio A** (1.3) y de la **Sección 6.2** (Filosofía de Textura). + +**Evitar explícitamente**: (1) su paleta específica — dorados, rojos y morados saturados de forma generosa en casi toda su iconografía; en WGLB el dorado saturado está racionado a lo derivado de permadeath (Principio A). (2) Su densidad ornamental casi total — la Sección 6.3 exige que la densidad de props responda al mood-state, no a un maximalismo constante. (3) Sus momentos de jefe con múltiples fuentes de luz saturada compitiendo entre sí — WGLB reserva la competencia visual de alta saturación exclusivamente para la aparición de Oro Reliquia. + +### 9.2 Dark Souls (videojuego) + +**Extraer específicamente**: su storytelling ambiental a través de la escala relacional — el personaje del jugador puesto en escena como figura pequeña y sobre-armada, empequeñecida deliberadamente por arquitectura y jefes. Referencia directa de **Principio B** (1.3) y de la jerarquía de escala de la **Sección 3.4**. + +**Evitar explícitamente**: (1) su paleta dominante de gris/marrón de baja saturación aplicada indiscriminadamente a *todo* el juego — WGLB necesita que el Oro Reliquia corte de inmediato a través de un mundo apagado. (2) Su narrativa exclusivamente ambiental/textual sin personajes vivos previos al apego del jugador — WGLB depende de conocer y encariñarse con un héroe antes del sacrificio (Pilar 1). (3) Su producción 3D naturalista con animación realista de peso — WGLB es 2D, de silueta-primero y poses tipo estatuaria (5.3). + +### 9.3 Hollow Knight (videojuego) + +**Extraer específicamente**: su contención al escenificar momentos emocionalmente pesados — sostener una silueta quieta y legible en espacio negativo en vez de cortar a negro o a cinemática pasiva. Referencia directa de **Principio C** (1.3) y de la excepción de pose dinámica en la **Sección 5.3**. + +**Evitar explícitamente**: (1) su paleta fría azul-negra aplicada casi uniformemente a *todo* el juego — en WGLB esa quietud fría es exclusiva del mood-state de Muerte/Sacrificio (2.3). (2) Su lenguaje de forma de criatura suave, redondeada, tipo insecto — WGLB es angular y dentado por regla dura (3.2). (3) Su escala de exploración plataformera de un solo personaje — no aporta nada a la legibilidad de silueta a escala RTS con muchas unidades en pantalla. + +### 9.4 Shadow of the Colossus (videojuego) + +**Extraer específicamente**: la identidad material de los colosos como arquitectura/ruina literalmente viva — piedra antigua, musgo, mampostería fusionada con una criatura que respira, de modo que el asombro viene de que el jugador puede leer "esto fue construido/tallado, y ahora se mueve". Referencia directa para el lenguaje de materiales de la **Sección 5.1** (piedra, vidrio emplomado, metal votivo oxidado fusionados con corrupción orgánica) y para el "territorio del kaiju" de la **Sección 3.2**. + +**Evitar explícitamente**: (1) su paleta desértica, cálida y naturalista, y la ausencia casi total de corrupción/malicia en el diseño de los colosos (se leen como trágicos o neutrales, nunca sacrílegos) — el kaiju de WGLB debe leerse como corrupción divina activa; mantener el lavado Acero Violeta-Ceniza (4.2) como marcador constante de amenaza. (2) Su estructura de ritmo de "un coloso, un duelo aislado, sin otras unidades en pantalla" — los encuentros de kaiju en WGLB ocurren dentro de una batalla RTS más amplia con tropas y héroes simultáneos (3.4). + +### 9.5 Godzilla (franchise/cine) + +**Extraer específicamente**: su lenguaje icónico de silueta de kaiju y, sobre todo, sus estados de daño/corrupción luminosos (espinas dorsales que se iluminan antes de un ataque, fisuras que resplandecen) — referencia directa para las "fisuras que brillan" del lenguaje de materiales de la **Sección 5.1** y para la idea de un kaiju como fuerza de la naturaleza/dios antiguo que ninguna sociedad puede controlar del todo, reforzando el read "divino-corrompido". + +**Evitar explícitamente**: (1) su convención de silueta bípeda, más reptiliana/humanoide — el kaiju de WGLB debe permanecer horizontal, bajo y de escala-arquitectura (Sección 3.1), nunca leerse como "una persona muy grande". (2) Su registro tonal de ciencia ficción/alegoría nuclear — el kaiju de WGLB es sagrado-gótico, no de ciencia ficción; evitar cualquier iconografía industrial/atómica que no encaje con el fresco de vitral. + +### 9.6 Pacific Rim (cine) + +**Extraer específicamente**: su puesta en escena de contraste de escala — cómo la cámara y el encuadre comunican dominancia de escala comparando al kaiju con elementos de escala humana (edificios, personas) en el mismo plano, reforzando el Principio B y la jerarquía "el kaiju primero, siempre" de la **Sección 3.4**. También su noción de que los ataques del kaiju dejan daño persistente en el mundo, reforzando el mecanismo de grietas-cicatriz de pantalla de la **Sección 2.2**. + +**Evitar explícitamente**: (1) su estética urbana saturada de neón, lluvia y color — un registro de paleta completamente distinto al gótico apagado de WGLB (Sección 4.1); nada de neón ni saturación urbana debe filtrarse al mundo. (2) Su respuesta de "máquina/mecha a escala equivalente" al kaiju — la respuesta de WGLB a la amenaza son héroes mortales pequeños y ornamentados (Sección 5.1), no armas de guerra escaladas al tamaño del kaiju. + +### 9.7 Distinción Explícita Entre Referencias (Anti-Redundancia) + +- **Blasphemous 2 vs. Hollow Knight**: ambos son pixel art atmosférico 2D, pero Blasphemous 2 aporta *disciplina de iluminación/textura pintada* (cómo se pinta una superficie); Hollow Knight aporta *disciplina de puesta en escena/cámara* (cómo se encuadra un momento emocional). +- **Dark Souls vs. Shadow of the Colossus**: Dark Souls aporta la *relación de escala jugador-amenaza en general* (aplica a arquitectura, jefes, personaje del jugador); Shadow of the Colossus aporta específicamente la *identidad material del kaiju en sí* (de qué está hecho, cómo se lee su corrupción). +- **Shadow of the Colossus vs. Godzilla**: ambos informan al kaiju, pero Shadow of the Colossus cubre su *material base en reposo* (piedra/musgo/mampostería fusionada), mientras Godzilla cubre específicamente sus *estados de daño/activación* (fisuras y espinas que se iluminan durante el ataque) — uno es reposo, el otro es evento. +- **Godzilla vs. Pacific Rim**: Godzilla informa la *identidad del propio kaiju* (silueta icónica, luminosidad de corrupción); Pacific Rim informa la *puesta en escena de la cámara alrededor de él* (cómo se compone un plano para que la escala se sienta real), no el diseño del kaiju en sí. +- **Hollow Knight vs. Pacific Rim**: Hollow Knight informa el staging de *momentos íntimos de muerte* (silueta quieta, espacio negativo); Pacific Rim informa el staging de *momentos de escala masiva* (comparación kaiju-vs-mundo) — polos opuestos de la misma disciplina de composición. + +### 9.8 Resumen de Cobertura + +| Referencia | Principio/Sección que respalda | Qué NO cubre (buscar en otra referencia) | +|---|---|---| +| Blasphemous 2 | Principio A, Sección 6.2 (iluminación pictórica de textura) | Escala, staging de muerte, kaiju — ver Dark Souls, Hollow Knight, Shadow of the Colossus | +| Dark Souls | Principio B, Sección 3.4 (relación de escala) | Material/identidad del kaiju — ver Shadow of the Colossus, Godzilla | +| Hollow Knight | Principio C, Sección 5.3 (staging de muerte) | Iluminación de textura, escala, kaiju — ver Blasphemous 2, Dark Souls, Pacific Rim | +| Shadow of the Colossus | Sección 5.1, 3.2 (material base del kaiju en reposo) | Estados de daño/activación — ver Godzilla | +| Godzilla | Sección 5.1 (fisuras luminosas, read divino-corrompido) | Puesta en escena de cámara — ver Pacific Rim | +| Pacific Rim | Sección 3.4, 2.2 (puesta en escena de escala, daño persistente) | Identidad/material del kaiju — ver Shadow of the Colossus, Godzilla | + +*(Nota: se evaluó también Pentiment como referencia para la disciplina de producción de UI/iconografía de las Secciones 7.1-7.2, pero se descartó — esas secciones quedan respaldadas solo por su descripción textual, sin referencia visual externa asignada.)* + diff --git a/design/gdd/combate-dano.md b/design/gdd/combate-dano.md new file mode 100644 index 0000000000..cf1c5ebe90 --- /dev/null +++ b/design/gdd/combate-dano.md @@ -0,0 +1,484 @@ +# Combate/Daño + +> **Status**: In Design +> **Author**: usuario + agentes +> **Last Updated**: 2026-08-09 +> **Implements Pillar**: Preparación Ritual, Clímax Explosivo (Pilar 3); habilita Sacrificio con Peso (Pilar 1) + +## Overview + +Combate/Daño es el sistema de **resolución de combate** del juego: decide cuándo dos fuerzas entran en contacto, cuánto daño se inflige por golpe, cuándo una unidad muere, y —de forma única en este juego— **bajo qué circunstancias murió un héroe**. A nivel de datos, es la capa que las tropas, los héroes y (por contrato) el kaiju delegan para todo cálculo de daño: posee el contrato de **contacto/aggro** que dispara el estado de combate, aplica el **daño-por-golpe** que determina cuántos golpes matan a cada tipo de unidad, hace clamp de la vida a 0 en overkill, y notifica `DYING` (héroe) / `DEAD` (tropa). A nivel de jugador, es donde vive el clímax del **Pilar 3**: los choques breves e intensos contra el kaiju que la fase de Preparación anticipa. Pero su rol más importante es silencioso: es el sistema que **clasifica las circunstancias de la muerte de un héroe** —si fue deliberada (`D`), si sirvió un propósito (`P`)— y entrega ese veredicto a Permadeath, que lo convierte en la calidad de la reliquia. Sin Combate/Daño no habría clímax que resolver ni, más importante, un puente entre "un héroe murió" y "qué compró esa muerte" — es el sistema que hace que el **Pilar 1** tenga consecuencias mecánicas, no solo narrativas. + +## Player Fantasy + +**El jugador es el comandante en el choque decisivo.** El combate es el momento en que la Preparación cobra o pierde su sentido: las tropas posicionadas, los héroes comprometidos, todo se resuelve en enfrentamientos **breves, densos e intensos** (Pilar 3). No es la fantasía del micro frenético de APM alto — es la del general de Warcraft 3, con pocas unidades que importan, donde cada choque tiene peso porque las tropas son finitas y los héroes irremplazables. La satisfacción táctil de dos fuerzas colisionando, pero siempre al servicio de una aritmética mayor: no estás optimizando DPS, estás decidiendo qué se gasta y por qué. + +Su carga emocional más honda es que **el combate es donde la tragedia se ejecuta**. Cuando un héroe entra en combate, el jugador no está maximizando daño — está decidiendo, con las manos, qué compra esa vida. Aquí, en el choque, es donde la diferencia entre una muerte desperdiciada y una bien gastada se vuelve un acto concreto y no una abstracción: el sistema que traduce "cómo peleaste" en "qué reliquia nace" (`D`/`P`) vive precisamente en este momento. Combate/Daño es lo que hace que el Pilar 1 tenga dientes: sin él, "el sacrificio pesa" sería una frase; con él, es una decisión táctica que el jugador toma y siente. + +La honestidad de la fantasía descansa, otra vez, en la **legibilidad**: el jugador debe poder leer quién gana el choque, qué está muriendo y quién está en peligro, para que su decisión de comprometer o retirar sea informada y no una lotería. La tragedia legítima es la de decidir bajo un choque legible — no la de ser sorprendido por matemática opaca. Un combate cuyo resultado el jugador no pudo anticipar ni influir no es tensión táctica; es azar, y traiciona el Pilar 4 (Historia Jugada, No Contada). + +## Detailed Design + +### Core Rules + +1. **Autoridad única de daño**: *todo* daño en el juego (unidad↔unidad y kaiju↔unidad) se aplica a través de una única operación `apply_damage(target, amount, source)` que hace clamp de la vida a `[0, max_health]` y dispara la notificación de muerte. Un solo camino de daño y muerte — no hay vías paralelas. Los valores de ataque son data-driven (ver `CombatProfile`, Regla 2). **Combate resuelve en el tick de física (`_physics_process`, paso fijo), nunca en `_process`** — es la única forma de que los TTK exactos de Formulas (F3) sean reproducibles y no dependan del framerate. **Cada tick se resuelve en dos fases, en este orden estricto**: (1) se aplican *todos* los `apply_damage` pendientes del tick contra el estado de vida vigente al inicio del tick — ningún golpe de este tick ve el resultado de otro golpe del mismo tick; (2) solo cuando la fase (1) termina para *todos* los combatientes, se disparan *todas* las notificaciones de muerte (`DEAD`/`DYING`) y se computa `D`/`P` (Regla 8) contra el estado de mundo post-daño. Esto es lo que garantiza que un mutual kill simultáneo (ver Edge Cases) sea real — ninguna unidad puede leer o reaccionar a la muerte de otra que murió en el mismo tick antes de que la fase (2) empiece. + +2. **`CombatProfile` por tipo de unidad (owned por este sistema)**: cada tipo de unidad (tropa, héroe) y el kaiju tienen un `CombatProfile` con `attack_damage` (int), `attack_range` (world units), `attack_cooldown_s` (float), `aggro_range` (≥ `attack_range`) y `leash_range`. Estos son los stats de combate que Tropas/Héroes deliberadamente **no** poseen (Tropas difirió armadura/daño a este sistema). *(Dónde se almacena el `CombatProfile` — embebido en `TroopDefinition`/`HeroDefinition` vs. resource paralelo — es una decisión de arquitectura, ver Open Questions.)* + +3. **Ataque en tiempo real por cooldown**: cuando una unidad tiene un objetivo válido dentro de `attack_range` y su cooldown está listo, inflige `attack_damage` vía `apply_damage` y reinicia el cooldown (`attack_cooldown_s`). Sin proyectiles con viaje en el MVP (impacto instantáneo al resolver el golpe); el rol `ranged` se modela con mayor `attack_range`, no con física de proyectil. **El cooldown nunca se resetea por perder o cambiar de objetivo** — sigue corriendo igual en `NO_TARGET` que en cualquier otro estado (anti-exploit, ver Edge Cases). Al adquirir un objetivo (nuevo o el mismo tras reordenar) se aplica además un **piso de adquisición**: el primer golpe contra ese objetivo nunca cae antes de que transcurra un `attack_cooldown_s` completo desde el instante de esa adquisición, incluso si el cooldown heredado ya había llegado a 0 mientras la unidad estaba `NO_TARGET`. El atacante golpea cuando **ambas** condiciones se cumplen — cooldown heredado en 0 **y** piso de adquisición cumplido — cerrando tanto el reset-por-cambio-de-objetivo como el farmeo de golpes instantáneos por banking de cooldown en `NO_TARGET`. + +4. **Aggro híbrido con auto-persecución acotada**: una unidad **sin orden explícita** auto-adquiere al enemigo válido más cercano dentro de `aggro_range` y lo persigue hasta atacarlo, sin alejarse más de `leash_range` de su posición de retención (si el enemigo escapa el leash, la unidad vuelve a retener). Una **orden explícita** de Control (attack-target o attack-move) sobreescribe la adquisición automática: attack-target fija un objetivo hasta que muere/sale de rango o se reordena; attack-move avanza al destino auto-enganchando enemigos en el camino. **La posición de retención se fija en el instante en que la unidad entra a `TARGET_ACQUIRED`** (el momento de adquisición) y no se actualiza mientras dura esa persecución/enganche. Si la unidad mata a su objetivo y auto-adquiere uno nuevo (encadenando kills), la posición de retención se **re-fija** en ese nuevo instante de adquisición — cada persecución individual queda acotada a `leash_range` desde donde empezó, aunque una cadena de kills sucesivos puede desplazar a la unidad más allá del `leash_range` original en conjunto. + +5. **Daño plano por golpe (sin armadura en el MVP)**: el defensor pierde exactamente `attack_damage`; no hay mitigación, armadura ni multiplicadores de tipo. `kill_time = ceil(defender_HP / attacker_damage)` — legible por diseño (Player Fantasy). El overkill se descarta: la vida hace clamp a 0, nunca negativa. *(Hooks de armadura/tipo se dejan para post-MVP; ver Open Questions.)* + + **Excepciones nombradas a "sin multiplicadores / camino único" (reconciliación cross-GDD 2026-08-21):** + - **Furia del Kaiju (×1.5)** — la única excepción **multiplicativa** del proyecto (Encuentro con Kaiju Regla 10/F5, `attack_damage_effective = round(attack_damage × 1.5)`). Combate **no** aplica el multiplicador: el controlador del Kaiju computa el `amount` ya escalado y lo pasa por el camino único `apply_damage(target, amount, source)` (Regla 1 intacta). Los bonos de bendición **no** son multiplicativos — son planos aditivos vía la capa `CombatState` (ADR-0001), así que no amplían esta regla. + - **Clamp de expiración de bendición defensiva (F-RB4)** — cuando un modificador defensivo expira, `max_health` baja `−N` y `current_health = min(current_health, nuevo_max_health)` por **clamp directo**, NO vía `apply_damage`. Es un carve-out deliberado del camino único de la Regla 1, propiedad del barrido de expiración de `CombatState` (ADR-0001); "expirar nunca mata" (nunca dispara `DYING`). Reliquias AC-RB36. + +6. **Notificación de combate (Combate no posee el estado de la unidad)**: cuando una unidad adquiere objetivo, Combate notifica `ENGAGED` (tropa) / `ALIVE_ENGAGED` (héroe) a su sistema dueño; cuando pierde el objetivo y no hay otro en `aggro_range`, notifica el fin de combate. La vida y el estado de vida (IDLE/MOVING/ENGAGED/DEAD, ALIVE_*/DYING/DEAD) los **poseen** Tropas/Héroes; Combate solo dispara las transiciones y almacena la vida a través de la operación de daño. + +7. **Muerte por cruce de 0**: cuando `apply_damage` lleva la vida a ≤0, Combate hace clamp a 0 y notifica: + - **Tropa** → `DEAD`, **sin** capturar ningún dato de circunstancia (`killer_id`, ubicación, timestamp, causa) — Pilar 1, coherente con Tropas AC-06. + - **Héroe** → `DYING`, y **en ese mismo frame de cruce por 0 Combate computa y expone `D` y `P`** (Regla 8) para que el snapshot de Héroes los congele junto con `T` (que Héroes lee del Temporizador). + +8. **Clasificación `D`/`P` como clasificador puro (al morir un héroe)**: en el frame de muerte de un héroe, Combate **computa** los booleanos leyendo estado externo — no posee ni el verbo Sacrificio ni el marcado: + - **`D=1`** sii, leyendo el estado de comando de **Control/órdenes**: hay un comando **Sacrificio** activo/no-cancelado sobre este héroe **Y** la muerte ocurrió dentro de `sacrifice_radius` (=3.0, knob de Héroes) del destino ordenado. Si no, `D=0`. + - **`P=1`** sii, leyendo el estado marcado de **Encuentro con Kaiju**: la muerte ocurrió dentro de `guard_radius` (=4.0, knob de Héroes) de un objetivo/aliado marcado (vía **defensiva**) **O** el héroe neutralizó una amenaza crítica marcada del kaiju (vía **ofensiva**). Si no, `P=0`. + - Combate **no** computa `T` (eso lo provee el Temporizador y lo lee Héroes). Combate solo produce `D`/`P` y los entrega a Héroes, que los reenvía a Permadeath en su payload. + +9. **Combate no evalúa victoria/derrota de era** (mismo hueco abierto que Tropas y Héroes): solo resuelve daño y notifica muertes; quién declara el fin de la era es Encuentro con Kaiju o Transición de Era. + +### States and Transitions + +Combate **no** posee el ciclo de vida de la unidad (eso es de Tropas/Héroes). Lo que sí modela es el **ciclo de enganche de combate por combatiente**: + +| Estado de enganche | Descripción | Transición desde | Transición hacia | +|---|---|---|---| +| `NO_TARGET` | Sin objetivo; escaneando `aggro_range` (si sin orden) | Inicio; objetivo perdido; retorno por leash | `TARGET_ACQUIRED` | +| `TARGET_ACQUIRED` | Objetivo fijado (por aggro u orden); persiguiendo/posicionando si fuera de `attack_range` | `NO_TARGET`; reorden | `ATTACKING`, `NO_TARGET` (objetivo muere/escapa leash) | +| `ATTACKING` | Objetivo dentro de `attack_range`; infligiendo `attack_damage` cada `attack_cooldown_s` | `TARGET_ACQUIRED` | `TARGET_ACQUIRED` (objetivo sale de rango), `NO_TARGET` (objetivo muere) | + +- La entrada a `TARGET_ACQUIRED`/`ATTACKING` dispara la notificación `ENGAGED`/`ALIVE_ENGAGED` al sistema dueño; el retorno a `NO_TARGET` sin otro objetivo dispara el fin de combate. +- La **posición de retención** se fija en el instante de entrada a `TARGET_ACQUIRED` (Regla 4) — no se actualiza mientras la unidad persigue; se re-fija en cada nueva adquisición, incluida tras encadenar un kill. +- Este ciclo aplica igual a tropas, héroes y al kaiju (el kaiju es un combatiente con su propio `CombatProfile`, aunque su selección de objetivo la dirige la IA de Encuentro con Kaiju). + +### Interactions with Other Systems + +| Sistema | Dirección | Interfaz | +|---|---|---| +| Sistema de Tropas | bidireccional | Combate lee el `CombatProfile` de la tropa, aplica daño a su vida, y notifica `ENGAGED`/fin-de-combate/`DEAD` (sin data de circunstancia). Tropas posee vida y estado de vida. ✅ Diseñado | +| Sistema de Héroes | bidireccional | Igual que Tropas, y además: al cruzar 0, Combate computa y expone `D`/`P` para el snapshot de Héroes; notifica `DYING`. ✅ Diseñado | +| Control y Selección de Unidades | Control → Combate | Combate lee las órdenes explícitas de ataque (attack-target, attack-move) y el **estado del comando Sacrificio activo** por héroe (para `D`). Control posee la emisión de órdenes. ⚠️ **Parcial** — Control tiene GDD para selección/movimiento, pero el **comando Sacrificio y los verbos attack-target/attack-move aún NO están definidos** en `control-y-seleccion-de-unidades.md`; el lado `D` de la clasificación depende de esa adición (ver Open Questions OQ-2) | +| Encuentro con Kaiju | bidireccional | Combate aplica el daño kaiju→unidad y unidad→kaiju (autoridad centralizada); lee las definiciones de ataque del kaiju y el **estado de objetivos/amenazas marcadas** (para `P`). Kaiju posee su IA de objetivo y el marcado. ✅ **Approved** (`encuentro-con-kaiju.md`) — el marcado (`marked_targets`/`marked_threats`) lo produce Encuentro Regla 5 (AC-K43/K44); resuelve OQ-5/OQ-7/OQ-8 | +| Sistema de Héroes → Permadeath | indirecto | Combate provee `D`/`P`; Héroes los reenvía en su payload de muerte a Permadeath. Combate no habla con Permadeath directamente | +| Temporizador de Preparación/Ritual | ninguna directa | Combate **no** lee ni computa `T`; se aclara aquí para evitar la conflación — `T` lo provee el Temporizador y lo lee Héroes | +| Datos de Era/Civilización | Datos → Combate | Los `CombatProfile` se autoran por tipo de unidad del roster de la era; Combate los consume vía las definiciones de unidad | + +> **Nota de contrato desbloqueado**: esta sección resuelve el "contrato de contacto/aggro" que Tropas (AC-09/AC-11) y Héroes esperaban de este sistema — `ENGAGED` se dispara al entrar a `TARGET_ACQUIRED`/`ATTACKING`. + +## Formulas + +Combate resuelve daño con daño plano por golpe (sin armadura/mitigación en el MVP). Todas las unidades y el kaiju tienen un `CombatProfile`; las fórmulas convierten esos stats en tiempos de muerte legibles. + +### CombatProfile por tipo (valores MVP, tuning knobs por tipo) + +> **Capa de modificadores por instancia (ADR-0001, Accepted 2026-08-21).** El `CombatProfile` es **por tipo** e inmutable en runtime. Sobre él, Combate posee una capa **`CombatState` por instancia** (referencia solo-lectura al `CombatProfile` + lista de `StatModifier` planos aditivos). `apply_damage` lee el stat **efectivo** vía `effective_attack_damage()` (= base + Σ modificadores activos); Reliquias/Bendiciones deposita modificadores con `add_modifier`/`remove_by_source`. Combate barre los expirados con `sweep_expired(TimeControl.game_tick)` al inicio del tick, antes de la fase-1 de daño (Regla 1 intacta: toda la matemática de daño sigue dentro de Combate). Nunca se muta el `CombatProfile` compartido. Ver `docs/architecture/adr-0001-combat-stat-modifier-layer.md`. + +| Unidad | `attack_damage` | `attack_range` (wu) | `attack_cooldown_s` | DPS | `aggro_range` (wu) | `leash_range` (wu) | +|---|---|---|---|---|---|---| +| melee_infantry (40 HP) | 8 | 1.0 | 1.0 | 8.00 | 5.0 | 8.0 | +| ranged_troop (20 HP) | 5 | 4.0 | 1.2 | 4.17 | 6.0 | 9.0 | +| support_troop (28 HP) | 4 | 2.5 | 1.5 | 2.67 | 5.0 | 8.0 | +| Vanguardia (140 HP, ofensivo) | 25 | 1.2 | 0.9 | 27.8 | 7.0 | 12.0 | +| Portaestandarte (150 HP, rally) | 14 | 1.2 | 1.1 | 12.7 | 5.5 | 8.0 | +| Guardián (200 HP, defensivo) | 18 | 1.3 | 1.4 | 12.9 | 5.0 | 6.0 | +| Kaiju (aprox.) | 45 | 2.5 | 4.5 | 10.0 | 10.0 | provisional* | + +*Racional: tropas melee cambian rango por HP/daño; ranged cambia HP por alcance. Héroes son 3-18× más letales que tropas (fantasía: el héroe destroza masa — 2 golpes matan a un melee). Guardián tiene el leash más corto (aguanta la línea, no persigue — coherente con la vía defensiva de `P`); Vanguardia el más largo (skirmisher agresivo). *El leash del kaiju lo anula por completo Encuentro con Kaiju (Approved, Regla 2 — persecución ilimitada durante `CLIMAX`, el campo nunca se lee); el centinela `provisional*` refleja esa anulación, no un valor pendiente.* + +### F1 — `apply_damage` / clamp + +`health_after = clamp(health_before − attack_damage, 0, max_health)` + +| Variable | Símbolo | Tipo | Rango | Descripción | +|---|---|---|---|---| +| Vida antes | `health_before` | int | [0, max_health] | HP antes del golpe | +| Daño de ataque | `attack_damage` | int | ≥1 | Daño plano por golpe | +| Vida máxima | `max_health` | int | ≥1 | HP máximo de la unidad | +| **Vida después** | `health_after` | int | [0, max_health] | Resultado — clampeado, overkill descartado | + +**Ejemplo**: `health_before=5`, `attack_damage=45` (kaiju), `max_health=40` → `clamp(5−45, 0, 40) = 0`. + +### F2 — `kill_time_hits` + +`kill_time_hits = ceil(defender_HP / attacker_damage)` + +| Variable | Símbolo | Tipo | Rango | Descripción | +|---|---|---|---|---| +| Vida del defensor | `defender_HP` | int | ≥1 | HP al iniciar el enganche | +| Daño del atacante | `attacker_damage` | int | ≥1 | `attack_damage` del atacante | +| **Golpes para matar** | `kill_time_hits` | int | ≥1 | Golpes requeridos (sin cota superior) | + +**Ejemplo**: tropa melee (40 HP) vs melee (dmg 8) → `ceil(40/8) = 5` golpes. + +### F3 — `time_to_kill_s` + +`time_to_kill_s = kill_time_hits × attacker_cooldown_s` + +| Variable | Símbolo | Tipo | Rango | Descripción | +|---|---|---|---|---| +| Golpes para matar | `kill_time_hits` | int | ≥1 | De F2 | +| Cooldown del atacante | `attacker_cooldown_s` | float | >0 | `attack_cooldown_s` | +| **Tiempo para matar** | `time_to_kill_s` | float | ≥cooldown | Segundos de reloj hasta la muerte | + +**Convención (confirmada — godot-specialist, ver Regla 1/Regla 3)**: el cooldown **no** está pre-cargado — el primer golpe cae tras un cooldown completo, no instantáneo al adquirir; se aplica como piso de adquisición sobre el tick de física (`_physics_process`), no sobre framerate variable. La lectura "listo-al-adquirir" restaría un cooldown a cada TTK. **Ejemplo**: 5 golpes × 1.0s = 5.0s. + +### F4 — `dps` + +`dps = attack_damage / attack_cooldown_s` (>0, sin cota superior). **Ejemplo**: kaiju 45/4.5 = 10.0. + +### F5 — Invariante de rangos + +`attack_range ≤ aggro_range ≤ leash_range` + +Si `aggro_range < attack_range` hay una **zona muerta**: un enemigo entre ambos radios está en rango de golpe pero nunca se auto-adquiere. Si `leash_range < aggro_range`, una unidad que auto-adquiere no puede perseguir (vuelve al instante). Ambos casos → loud-fail al cargar. + +**Excepción nombrada — Kaiju**: el `leash_range` del Kaiju es un valor centinela (`provisional*`, ver tabla `CombatProfile`) hasta que Encuentro con Kaiju defina su IA de persecución (OQ-7). Mientras ese campo permanezca marcado como centinela, F5 **no** evalúa el tramo `≤ leash_range` para el Kaiju — el loader lo excluye explícitamente de ese chequeo puntual, no como una omisión silenciosa sino como la única excepción nombrada a la regla de Enforcement. El tramo `attack_range ≤ aggro_range` del Kaiju sigue validándose con loud-fail normal. La excepción se retira en cuanto Encuentro con Kaiju asigne un `leash_range` real. + +### Matriz de time-to-kill (segundos) + +| Enfrentamiento | HP | dmg | cd | golpes | TTK | +|---|---|---|---|---|---| +| melee vs melee | 40 | 8 | 1.0 | 5 | **5.0s** | +| ranged vs ranged | 20 | 5 | 1.2 | 4 | **4.8s** | +| Vanguardia → tropa melee | 40 | 25 | 0.9 | 2 | **1.8s** | +| kaiju → tropa | 20-40 | 45 | 4.5 | 1 | **4.5s** (casi instantáneo, intencional) | +| kaiju → Vanguardia/Portaestandarte | 140/150 | 45 | 4.5 | 4 | **18.0s** | +| kaiju → Guardián | 200 | 45 | 4.5 | 5 | **22.5s** | + +Los enfrentamientos son breves e intensos (Pilar 3) y legibles (kill_time es aritmética simple). El kaiju one-shotea tropas y tarda 4-5 golpes en un héroe. + +### Enforcement (loud-fail, precedente de Héroes — no clamp) + +Cada campo de `CombatProfile` fuera de su dominio válido **falla ruidosamente al cargar** (no se clampa) — cada caso es una fórmula rota, no un extremo válido: +- `attack_damage ≥ 1` (0 = objetivo inmatable) +- `attack_cooldown_s ≥ 0.1` (0 = DPS ÷0 / exploit de DPS infinito) +- invariante F5 `attack_range ≤ aggro_range ≤ leash_range` — **única excepción nombrada**: el tramo `≤ leash_range` del Kaiju queda exento mientras su `leash_range` sea el centinela `provisional*` (ver F5); la excepción se retira cuando Encuentro con Kaiju lo defina +- sin valores negativos + +### Nota cross-GDD + +Los baselines de HP de tropas (melee 40 = 2× ranged; support 28 = 1.4× ranged) **se sostienen** contra estos números — los mirror-matchups quedan a ~5s entre sí. **Se recomienda des-flaggear** la nota provisional de HP en Sistema de Tropas (no requiere revisión). Ver Phase 5 / Open Questions. + +## Edge Cases + +- **Config inválido de `CombatProfile`** (`attack_damage < 1`, `attack_cooldown_s < 0.1`, invariante F5 violado `attack_range ≤ aggro_range ≤ leash_range`, o cualquier negativo): **loud-fail al cargar** (no clamp) — cada caso es una fórmula rota (objetivo inmatable, DPS ÷0, zona muerta de aggro), no un extremo válido. +- **`apply_damage` sobre una unidad ya `DEAD`/`DYING`**: se rechaza (no-op) — no se puede dañar un cadáver ni re-disparar la muerte de un héroe ya en `DYING`. Coherente con Héroes AC-H27b (`DEAD` es inaccionable). +- **Sin fuego amigo en el MVP**: `apply_damage` solo se aplica entre facciones opuestas (jugador↔kaiju/esbirros). Una unidad nunca daña a una aliada; los ataques solo adquieren enemigos válidos. +- **Dos combatientes se matan en el mismo frame** (golpes letales simultáneos): ambos `apply_damage` se resuelven; ambos mueren. Si ambos son héroes, ambos entran a `DYING` y **Permadeath serializa los beats** (contrato de Héroes) — Combate solo aplica ambos daños, no ordena los beats. +- **El objetivo muere a mitad del cooldown del atacante**: el atacante vuelve a `NO_TARGET` (o auto-adquiere el siguiente enemigo en `aggro_range`), pero **su cooldown en curso sigue corriendo** — cambiar de objetivo **no** refresca un cooldown a medio cargar. Cierra el exploit de "reset de cooldown por target-switching". +- **El objetivo sale de `attack_range` a mitad del cooldown**: el atacante lo persigue (dentro de `leash_range`) y reanuda al reentrar en rango; el cooldown sigue corriendo. Si el objetivo escapa `leash_range`, el atacante vuelve a su posición de retención y a `NO_TARGET`. +- **Orden attack-target cuyo objetivo muere**: la orden se consume; la unidad revierte a auto-aggro (comportamiento idle). Una orden attack-move, en cambio, continúa hacia su destino auto-enganchando en el camino. +- **Un héroe muere sin comando Sacrificio ni objetivo/amenaza marcada** (`D=0, P=0`): Combate reporta `D=0, P=0`; la reliquia resultante es Tier 1 Delgada (válvula anti-nihilismo de Héroes). No es un bug — es el sistema funcionando; Combate solo reporta la clasificación, no la juzga. +- **El kaiju llega a 0 de vida** (daño unidad→kaiju): Combate aplica el daño y notifica que el kaiju cruzó 0, pero **no declara victoria** — la evaluación de fin de era la posee Encuentro con Kaiju / Transición de Era (Regla 9, mismo hueco que Tropas/Héroes). +- **Sin línea de visión en el MVP**: `attack_range` es distancia pura; no hay chequeo de line-of-sight (un ranged puede disparar "a través" de obstáculos). Simplificación deliberada del MVP por legibilidad; hook de LoS diferido (ver Open Questions). +- **Resolución de combate durante el beat `DEATH_HOLD`**: si el beat de muerte de héroe pausa el tiempo de juego (timing propiedad de Permadeath), los cooldowns de combate se pausan con él — Combate sigue la misma escala de tiempo de juego que el Temporizador (independiente de framerate/tiempo real). Si el beat no pausa el tiempo, el combate continúa. Contrato a confirmar con Permadeath. + +## Dependencies + +**Dependencias hacia arriba (upstream) — lo que este sistema necesita:** + +| Sistema | Tipo | Interfaz | Estado | +|---|---|---|---| +| Sistema de Tropas | Dura (bidireccional) | Combate lee el `CombatProfile` de la tropa, aplica daño, notifica `ENGAGED`/fin-de-combate/`DEAD`. Tropas posee vida y estado | ✅ Diseñado — **resuelve Tropas AC-09/AC-11** (el contrato de contacto que esperaban) | +| Sistema de Héroes | Dura (bidireccional) | Igual que Tropas, y computa/expone `D`/`P` al `DYING` | ✅ Diseñado — **formaliza el contrato de clasificación `D`/`P`** que Héroes esperaba | +| Control y Selección de Unidades | Dura | Combate lee órdenes de ataque (attack-target/attack-move) y el estado del comando **Sacrificio** activo por héroe (para `D`) | ⚠️ **Parcial** — GDD existe para selección/movimiento, pero el comando Sacrificio y los verbos attack-target/attack-move **no están definidos aún** en su GDD; el lado `D` los necesita (ver OQ-2) | +| Datos de Era/Civilización | Blanda (datos) | Los `CombatProfile` se autoran por tipo de unidad del roster de la era; Combate los consume vía las definiciones | ✅ Diseñado | + +**Dependientes hacia abajo (downstream — dependen de este):** + +| Sistema | Tipo | Interfaz | +|---|---|---| +| Encuentro con Kaiju | Dura (bidireccional) | Combate aplica el daño kaiju↔unidad (autoridad centralizada); lee las defs de ataque del kaiju y el estado de marcado (para `P`). Kaiju posee su IA y el marcado. ✅ **Approved** — contrato real en `encuentro-con-kaiju.md` (Regla 3 selección de objetivo, Regla 5 marcado, Regla 9 victoria/derrota) | +| Permadeath | Dura (indirecto) | Recibe `D`/`P` a través del payload de muerte que Héroes le reenvía; Combate no habla con Permadeath directamente. *Sin GDD* | +| Reliquias/Bendiciones | Dura (API de modificadores, ADR-0001) | Combate posee una capa `CombatState` **por instancia** sobre el `CombatProfile` por tipo: expone `effective_attack_damage()` (read-hook leído internamente en `apply_damage`) y la API de escritura `add_modifier`/`remove_by_source`/`sweep_expired`. Reliquias deposita modificadores planos aditivos vía esa API y nunca escribe `CombatProfile`. Combate posee la aplicación y la expiración (`sweep_expired(TimeControl.game_tick)` al inicio del tick, antes de fase-1). ✅ Definido — `docs/architecture/adr-0001-combat-stat-modifier-layer.md` (Accepted) + `reliquias-bendiciones.md` | +| UI/HUD | Dura | Renderiza vida, feedback de daño y estado de combate desde lo que este sistema expone. *Sin GDD* | + +**Nota cross-GDD (acción Phase 5)**: Combate confirma que los baselines de HP de tropas se sostienen — se recomienda **des-flaggear** la nota provisional de HP en `sistema-de-tropas.md` (Formulas y Open Questions). Se aplicará en la fase de registro. + +**Nota (actualizada 2026-08-21)**: Encuentro con Kaiju, Permadeath, Reliquias/Bendiciones y UI/HUD **ya tienen GDD**; sus contratos con este sistema están confirmados. Los dos que tocan a Combate a nivel de arquitectura están cerrados por ADRs Accepted: la capa de modificadores por instancia (Reliquias) por **ADR-0001**, y la fuente de `game_time_paused`/tick por **ADR-0002** (servicio TimeControl). Tropas, Héroes, Control y Datos de Era ya estaban confirmados. + +## Tuning Knobs + +Los knobs de este sistema son los cinco campos del `CombatProfile`, **por tipo de unidad** (viven en cada Resource, como los stats de Tropas — son contenido, no sliders globales). Valores por defecto en la tabla de Formulas. + +| Knob | Tipo | Rango seguro | Qué se rompe en extremos | +|---|---|---|---| +| `attack_damage` | int (por tipo) | ≥ 1 | <1: objetivo inmatable (**loud-fail**); demasiado alto: one-shots que rompen la legibilidad de `kill_time` y el "breve pero con tensión" del Pilar 3 | +| `attack_cooldown_s` | float (por tipo) | ≥ 0.1 | <0.1: DPS ÷0 / DPS efectivamente infinito (**loud-fail**); demasiado alto: el combate se arrastra, pierde el "intenso" del Pilar 3 | +| `attack_range` | float wu (por tipo) | >0, ≤ `aggro_range` | demasiado corto: melee nunca alcanza; demasiado largo: ranged dispara desde fuera de pantalla, ilegible | +| `aggro_range` | float wu (por tipo) | `attack_range` ≤ x ≤ `leash_range` | < `attack_range`: zona muerta de aggro (**loud-fail**, F5); demasiado grande: las unidades enganchan todo el mapa, caos ingobernable | +| `leash_range` | float wu (por tipo) | ≥ `aggro_range` | < `aggro_range`: no puede perseguir lo que adquiere (**loud-fail**, F5); demasiado grande: las unidades se alejan persiguiendo y la formación se dispersa | + +**Interacción entre knobs**: los tres rangos están acoplados por el invariante F5 (`attack_range ≤ aggro_range ≤ leash_range`). `kill_time` depende de `attack_damage` contra el HP del objetivo (que poseen Tropas/Héroes) — cambiar HP en esos GDDs re-balancea el combate aquí; coordinar cambios cross-GDD. El Kaiju es la única excepción nombrada al acoplamiento — su `leash_range` queda exento de F5 mientras sea el centinela `provisional*` (ver Formulas F5, OQ-7). + +**Enforcement**: **loud-fail al cargar** para todos los casos fuera de dominio (precedente de Héroes; no clamp) — ver la sección Enforcement de Formulas. Cada violación es una fórmula rota, no un extremo de feel. + +**Constantes consumidas (no se duplican)**: HP de unidades (`melee_infantry`=40, `ranged_troop`=20, `support_troop`=28, héroes 120-200 — de Tropas/Héroes); `sacrifice_radius`=3.0 y `guard_radius`=4.0 (de Héroes, aplicados por Combate en la clasificación `D`/`P`). + +## Visual/Audio Requirements + +*Referencia rectora: Art Bible Secciones 2.2 (Clímax de Combate), 2.3 (Muerte/Sacrificio), 3.4 (jerarquía visual del ojo), 4.2 (uso semántico del color), 5.4/8.4 (niveles de LOD), 7.0/7.5 (UI diegética). Audio es un **contrato provisional** — mismo estatus que Héroes A-1..A-3 y Temporizador — sin audio bible ni dirección de audio aún.* + +### Visual + +**CV-1 — Hit feedback en la silueta, no en overlay (art bible 4.2).** Cada golpe dispara un destello breve de un frame sobre la propia silueta del objetivo, reutilizando el drenaje Hueso→Sangre Vieja (4.2) ya usado en las barras de vida — el sprite mismo "sangra" el mismo lenguaje de color que su barra. Sin flash blanco genérico ni números de daño flotantes: a escala 24-32px con muchas unidades trocando golpes, ese ruido rompería la legibilidad justo cuando más se necesita. + +**CV-2 — Muerte de tropa: LOD-1, sin beat (art bible 5.4, 8.4; Pilar 1).** `DEAD` dispara la animación de caída/disolución de Nivel 1 ya incluida en el set base de toda tropa (8.4) — dura lo mismo que cualquier transición de estado, sin pausa de cámara, sin luz dedicada, sin desaturación. Deliberado, no recorte: Combate no captura circunstancia de muerte de tropa (Regla 7), así que no hay dato que un tratamiento sostenido pudiera mostrar. Cualquier VFX futuro que le dé pausa/cámara-lenta a una muerte de tropa se rechaza en review — es el fallo exacto que el Pilar 1 prohíbe. + +**CV-3 — Muerte de héroe: Combate dispara el cruce, no escenifica el beat (Héroes V-5/V-6, art bible 2.3).** Al cruzar 0, Combate notifica `DYING` — ahí termina su responsabilidad visual. La puesta en escena sostenida (luz clave, desaturación circundante, silueta agrietándose en vitral) es de Permadeath. Lo único que Combate garantiza: el estado de enganche del héroe se congela con él en ese frame — ningún atacante/objetivo sigue animando un intercambio contra una unidad ya en `DYING`. + +**CV-4 — Golpe de kaiju: contraste brutal y telegrafiado (Principio B, art bible 2.2, 9.5/9.6).** El impacto nunca suaviza la desproporción de escala para "encajar" visualmente. Con `attack_cooldown_s`=4.5s el ataque debe telegrafiarse antes de caer (fisuras/espinas iluminándose) — el jugador lee "esto va a golpear" con tiempo de reacción; la tragedia es de decisión, no de sorpresa. + +**CV-5 — Legibilidad de clash y su límite con UI/HUD (art bible 3.4, 7.5).** Quién gana un choque se lee por la jerarquía de forma (kaiju>héroe>masa) y por CV-1, no por barras flotantes constantes. **Límite**: Combate expone el dato (vida, estado de enganche) y el feedback en la silueta; cómo/cuándo UI/HUD superpone barras diegéticas (siempre vs. solo bajo umbral, 7.5) es responsabilidad de UI/HUD. + +### Audio *(provisional)* + +**CA-1 — Impacto melee vs ranged distinto.** Percusivo-cercano vs. ligero-a-distancia, reforzando `attack_range` sin mirar el sprite; el golpe de kaiju es categóricamente más grave/grande, reforzando CV-4. + +**CA-2 — Muerte de tropa tenue, masa en clashes.** Tenue individualmente, se vuelve masa/textura en clashes con muertes simultáneas (espejo de Tropas) — nunca compite con CA-3. + +**CA-3 — Muerte de héroe es handoff, no verbo propio de Combate.** Al notificar `DYING`, el audio pasa a Permadeath (Héroes A-1/A-2 — filtrado del lecho, understatement, silencio permitido). Combate solo garantiza que su propio lecho de combate se ducke primero al dispararse el beat. + +**Principios rectores**: A (art bible 4.2, drenaje de color — nunca dorado en combate rutinario), B (2.2/3.4/9.5-9.6, escala del kaiju), C (2.3, el beat que Combate dispara pero no escenifica), 5.4/8.4 (LOD-1 rutinario, LOD-2 exclusivo de permadeath), 7.0/7.5 (frontera con UI/HUD). + +📌 **Asset Spec** — Con Visual/Audio definido, tras aprobar el art bible se puede correr `/asset-spec system:combate-dano` para producir specs de VFX por-asset (hit flash, muerte de tropa, telegrafío de kaiju). + +## UI Requirements + +*Combate no diseña pantallas — expone datos que UI/HUD renderiza con el lenguaje diegético del art bible (Sección 7). Esta sección fija el contrato de qué datos fluyen, no cómo se ven.* + +**CU-1 — Exposición de estado por unidad para UI/HUD.** Combate expone, por unidad: `current_health`/`max_health`, el estado de enganche (`NO_TARGET`/`TARGET_ACQUIRED`/`ATTACKING`) y los eventos de daño. UI/HUD consume esto para renderizar barras de vida (drenaje Hueso→Sangre Vieja, art bible 4.2) e indicadores de combate. El *cuándo* mostrar barras (siempre vs. bajo umbral) lo decide UI/HUD (art bible 7.5). + +**CU-2 — Evento de golpe/impacto.** Cada resolución de golpe emite un evento que UI/HUD y VFX enganchan para el hit feedback (CV-1). Combate provee el disparador (quién golpeó a quién, cuánto), no el render. + +**CU-3 — Tasa de daño entrante (para estimaciones downstream).** Combate expone el daño entrante reciente por unidad. Esto es un **insumo** que Encuentro con Kaiju puede usar para su proyección de `time_to_death` (el trigger del readout de veredicto de Héroes U-3/AC-H29) — pero la proyección misma la posee Kaiju, no Combate (ver la tensión de la ventana de ~30s en Open Questions). Combate solo provee el dato crudo. + +**CU-4 — Sin editorializar el resultado de combate.** Coherente con el understatement del juego: Combate no expone texto de "¡Victoria!"/"¡Baja!" ni juicios — solo estado numérico y eventos. La lectura del choque es visual (CV-5), no textual. + +📌 **UX Flag — Combate/Daño**: Este sistema alimenta UI de combate (barras de vida, feedback de daño, estado de enganche). En Pre-Producción, correr `/ux-design` para el HUD de combate **antes** de escribir epics; las stories que referencien esa UI deben citar el spec de UX, no este GDD. + +## Acceptance Criteria + +*Convención: cada AC usa formato Dado/Cuando/Entonces y una etiqueta de tipo de evidencia (`[Logic/unit]`, `[Integration]`). Los AC marcados **(bloqueado)** dependen de un sistema — o de una porción no escrita de un sistema — sin contrato confirmado hoy; solo verificables mediante un mock en el límite de este sistema. Salvo indicación, los valores de ejemplo usan la tabla `CombatProfile` MVP (Formulas). (La numeración salta C15 y C49 — entradas reservadas eliminadas.)* + +### A. Reglas Core (1-9) + +**AC-C01 — Autoridad única de daño** **[Logic/unit]** +Dado cualquier fuente de daño (unidad↔unidad, kaiju↔unidad), cuando se resuelve un golpe, entonces la única vía por la que `health_current` decrece es `apply_damage(target, amount, source)` — no existe otro camino de código que mute la vida directamente. + +**AC-C02 — Clamp de `apply_damage` a `[0, max_health]`** **[Logic/unit]** +Dado `apply_damage` con cualquier `amount ≥ 1`, cuando `health_before − amount` cae fuera de `[0, max_health]`, entonces `health_after` se clampa estrictamente a ese rango — nunca negativo. + +**AC-C03 — `CombatProfile` data-driven, sin hardcode** **[Logic/unit]** +Dado un `CombatProfile` poblado para tropa, héroe o kaiju, cuando Combate resuelve un golpe o adquisición para esa unidad, entonces usa exclusivamente esos valores del resource — cambiar el `.tres` cambia el comportamiento sin tocar código. El kaiju usa el mismo ciclo de enganche (solo su selección de objetivo la dirige la IA de Kaiju). + +**AC-C04 — Ataque por cooldown: golpe al expirar, reset** **[Logic/unit]** +Dado un atacante con objetivo válido dentro de `attack_range` y cooldown listo, cuando se procesa el tick de combate, entonces se invoca `apply_damage` con `attack_damage` y el cooldown se reinicia a `attack_cooldown_s`. + +**AC-C05 — Cooldown no precargado al adquirir (convención F3, piso de adquisición)** **[Logic/unit]** +Dado un atacante que acaba de adquirir un objetivo, cuando pasa el primer tick con el objetivo en rango, entonces el primer golpe NO cae instantáneamente — transcurre un `attack_cooldown_s` completo antes del primer `apply_damage`. Esto se cumple **incluso si el atacante traía un cooldown heredado ya en 0** desde un objetivo anterior (piso de adquisición, Regla 3) — adquirir nunca produce un golpe instantáneo, sin importar el estado del cooldown heredado. + +**AC-C06 — Auto-aggro: adquiere el enemigo válido más cercano en `aggro_range`** **[Logic/unit]** +Dado una unidad sin orden explícita y ≥1 enemigo válido dentro de `aggro_range`, cuando se evalúa la adquisición, entonces se selecciona el más cercano — no el primero detectado ni uno aleatorio. + +**AC-C07 — Leash: retorno a retención si el objetivo escapa `leash_range`** **[Logic/unit]** +Dado una unidad persiguiendo un objetivo auto-adquirido, cuando la distancia a su posición de retención excede `leash_range`, entonces abandona la persecución, vuelve a retener y transiciona a `NO_TARGET`. + +**AC-C07b — Posición de retención se fija en la adquisición, se re-fija en cada re-adquisición** **[Logic/unit]** +Dado una unidad que adquiere un objetivo A en la posición X, cuando la unidad se desplaza persiguiendo a A, entonces su posición de retención permanece fija en X (no sigue al atacante); dado que la unidad mata a A y auto-adquiere un objetivo B en la posición Y, cuando comienza la persecución de B, entonces la posición de retención se re-fija a Y. + +**AC-C08 — Orden explícita sobreescribe auto-aggro** **[Integration]** +Dado una unidad con un enemigo auto-adquirido, cuando Control emite attack-target sobre otro enemigo, entonces la unidad abandona el auto-adquirido y persigue/ataca el ordenado hasta que muere, sale de rango o se reordena. + +**AC-C09 — attack-move auto-engancha en el trayecto** **[Integration]** +Dado una unidad con orden attack-move hacia un destino, cuando un enemigo entra en su `aggro_range`, entonces se engancha automáticamente sin abandonar la intención de attack-move (reanuda hacia el destino tras resolver/perder el enganche). + +**AC-C10 — Daño plano, sin armadura** **[Logic/unit]** +Dado un defensor con `health_current=H` y un atacante con `attack_damage=D`, cuando se aplica un golpe, entonces `health_current` decrece exactamente en `D`, sin mitigación ni multiplicador, sin importar el tipo de defensor. + +**AC-C11 — `ENGAGED`/`ALIVE_ENGAGED` notificado al adquirir; Combate no posee el estado** **[Integration]** +Dado una unidad que transiciona a `TARGET_ACQUIRED`/`ATTACKING`, cuando ocurre la transición, entonces Combate invoca únicamente el método de notificación del sistema dueño (Tropas/Héroes) — nunca escribe directamente el campo de estado de vida. + +**AC-C12 — Fin de combate notificado sin objetivo de reemplazo** **[Integration]** +Dado una unidad en combate cuyo objetivo muere o sale de `leash_range`, cuando no hay otro enemigo válido en `aggro_range`, entonces Combate notifica fin de combate y la unidad vuelve a `NO_TARGET`. + +**AC-C13 — Muerte de tropa: `DEAD` sin datos de circunstancia** **[Logic/unit]** +Dado una tropa cuya vida cruza 0, cuando `apply_damage` clampa a 0, entonces transiciona a `DEAD` y Combate no captura ni expone `killer_id`, ubicación, timestamp ni causa — ningún campo de circunstancia existe. + +**AC-C14 — Muerte de héroe: `DYING` + D/P computados y congelados en el mismo frame** **[Logic/unit]** +Dado un héroe cuya vida cruza 0 con entradas mockeadas arbitrarias de comando/marcado, cuando `apply_damage` clampa a 0, entonces transiciona a `DYING` y Combate computa `D`/`P` en ese mismo frame, congelándolos — no se releen en frames posteriores. (Verifica el snapshot-timing; la corrección de la clasificación está en el Grupo C.) + +**AC-C16 — Combate no evalúa victoria/derrota** **[Logic/unit]** +Dado el kaiju u otra unidad llegando a 0, cuando Combate procesa la muerte, entonces no emite ninguna señal ni llamada de evaluación de victoria/derrota — solo notifica la muerte individual. + +### B. Fórmulas (F1-F5) + +**AC-C17 — F1 exacto con overkill del kaiju** **[Logic/unit]** +Dado `health_before=5`, `attack_damage=45`, `max_health=40`, cuando se aplica `apply_damage`, entonces `health_after=0` exactamente. + +**AC-C18 — F1 invariante general** **[Logic/unit]** +Dado cualquier `health_before ∈ [0,max_health]` y `attack_damage ≥ 1` válidos, cuando se aplica `apply_damage`, entonces `health_after` siempre cae en `[0, max_health]`. + +**AC-C19 — F2 exacto: melee vs melee → 5 golpes** **[Logic/unit]** +Dado `defender_HP=40`, `attacker_damage=8`, cuando se calcula `kill_time_hits`, entonces el resultado es exactamente `5`. + +**AC-C20 — F2 redondea hacia arriba en división no exacta** **[Logic/unit]** +Dado `defender_HP=41`, `attacker_damage=8`, cuando se calcula `kill_time_hits`, entonces el resultado es `6`, nunca `5`. + +**AC-C21 — F3 exacto: TTK melee vs melee** **[Logic/unit]** +Dado `kill_time_hits=5`, `attacker_cooldown_s=1.0`, cuando se calcula `time_to_kill_s`, entonces el resultado es exactamente `5.0s`. + +**AC-C22 — F3 exacto: el kaiju one-shotea una tropa** **[Logic/unit]** +Dado `defender_HP=40` y el kaiju (`dmg=45, cd=4.5`), cuando se calculan F2/F3, entonces `kill_time_hits=1` y `time_to_kill_s=4.5s`. + +**AC-C23 — F3 exacto: matriz kaiju vs héroes** **[Logic/unit]** +Dado el kaiju contra `defender_HP=140/150` y `200`, cuando se calculan F2/F3, entonces `4 golpes / 18.0s` para 140 y 150 HP, y `5 golpes / 22.5s` para 200 HP. + +**AC-C24 — F4 exacto: DPS del kaiju** **[Logic/unit]** +Dado `attack_damage=45`, `attack_cooldown_s=4.5`, cuando se calcula `dps`, entonces el resultado es exactamente `10.0`. + +**AC-C25 — F5 invariante válido carga sin error** **[Logic/unit]** +Dado un `CombatProfile` con `attack_range=1.0 ≤ aggro_range=5.0 ≤ leash_range=8.0`, cuando se carga, entonces la carga se completa sin error. + +**AC-C26 — F5 loud-fail: zona muerta (`aggro_range < attack_range`)** **[Logic/unit]** +Dado un `CombatProfile` con `aggro_range < attack_range`, cuando se carga, entonces falla ruidosamente. + +**AC-C27 — F5 loud-fail: `leash_range < aggro_range`** **[Logic/unit]** +Dado un `CombatProfile` con `leash_range < aggro_range`, cuando se carga, entonces falla ruidosamente. + +**AC-C27b — F5 excepción nombrada: Kaiju con `leash_range` centinela carga sin error** **[Logic/unit]** +Dado el `CombatProfile` del Kaiju con `leash_range` marcado como el centinela `provisional*` y `attack_range ≤ aggro_range` válido, cuando se carga, entonces la carga se completa sin error — el tramo `≤ leash_range` del invariante F5 no se evalúa para este campo mientras sea centinela. Dado el mismo `CombatProfile` pero con `attack_range > aggro_range` (violación del tramo no exento), cuando se carga, entonces sigue fallando ruidosamente — la excepción cubre únicamente `leash_range`. + +### C. Clasificación D/P (Regla 8) — el contrato más intrincado del sistema + +*Nota: la tabla de Dependencias marca Control "✅ Diseñado", pero `control-y-seleccion-de-unidades.md` no define hoy el comando Sacrificio, su confirm-step, ni attack-target/attack-move como verbos distintos. Por eso los AC del lado `D` se tratan como **(bloqueado)**, igual que el lado `P` (Kaiju, sin GDD). Ver Open Questions OQ-2.* + +**AC-C28 — `D=1` sii las tres condiciones se cumplen** **(bloqueado — comando Sacrificio no definido en Control; ver OQ-2)** **[Logic/unit]** +*Mock Contract Assumptions: el mock de Control expone `sacrifice_command_active(hero_id) -> bool`, `sacrifice_command_destination(hero_id) -> Vector2`, y garantiza que el confirm-step ya se resolvió antes de marcar `active=true`. Re-verificar contra el GDD real cuando Control defina el comando.* +Dado un héroe con comando Sacrificio activo hacia destino X, cuando muere con el comando aún activo y dentro de `sacrifice_radius` de X, entonces `D=1`; si cualquiera de las tres condiciones falla, entonces `D=0`. + +**AC-C29 — `D=1` tolera knockback dentro del radio** **(bloqueado — mismo mock que AC-C28)** **[Logic/unit]** +Dado un héroe con Sacrificio activo hacia X, cuando un knockback lo desplaza del punto exacto pero muere dentro de `sacrifice_radius` de X, entonces `D=1`. + +**AC-C30 — `D=0` si el comando se cancela antes del frame de muerte** **(bloqueado)** **[Logic/unit]** +Dado un héroe cuyo comando Sacrificio se cancela en el frame N, cuando muere en el frame N+k (k>0), entonces `D=0` — se lee el estado del frame de cruce. + +**AC-C31 — `D=0` si el confirm-step no se completó** **(bloqueado)** **[Logic/unit]** +Dado un tap sin confirmar sobre Sacrificio (el mock nunca marca `active=true`), cuando el héroe muere, entonces `D=0`. + +**AC-C32 — `P=1` vía defensiva: muerte dentro de `guard_radius` de objetivo marcado** **(desbloqueado — Encuentro con Kaiju Approved)** **[Integration]** +*Contrato real (ya no mock): Encuentro con Kaiju expone `marked_targets`/`marked_threats` al entrar a `TELEGRAPHING` (Regla 5, AC-K43); `guard_radius=4.0` lo posee Héroes (Approved). Verificable sin mock contra ambos contratos reales, mismo patrón que AC-C50.* +Dado un objetivo/aliado marcado en posición Y (vía Encuentro Regla 5/AC-K43), cuando un héroe muere dentro de `guard_radius` de Y, entonces `P=1`. + +**AC-C33 — `P=1` vía ofensiva: neutralizar amenaza marcada** **(desbloqueado — Encuentro con Kaiju Approved)** **[Integration]** +*Contrato real: la amenaza marcada (`marked_threat`) y su neutralización las define Encuentro Regla 5/Regla 7 (AC-K44); el crédito en mutual-kill lo cubre AC-C42/Encuentro AC-K38.* +Dado una amenaza crítica marcada del kaiju (Encuentro Regla 5/AC-K44), cuando el héroe muere en la misma acción que la neutraliza, entonces `P=1` — mismo peso que la vía defensiva (nota de simetría H4). + +**AC-C34 — `P=0` si ninguna vía se cumple** **(desbloqueado — Encuentro con Kaiju Approved)** **[Logic/unit]** +Dado un héroe que muere sin estar en `guard_radius` de ningún marcado y sin neutralizar ninguna amenaza marcada (ambas vías definidas por Encuentro Regla 5), cuando se computa `P`, entonces `P=0`. + +**AC-C35 — Combate es clasificador puro (arquitectura)** **(parcial — lado Kaiju Approved; lado Control aún bloqueado por el comando Sacrificio no escrito, OQ-2)** **[Integration]** +Dado el cómputo de D/P en el frame de muerte, cuando se inspecciona qué estado escribe Combate, entonces Combate solo LEE los getters documentados de Control/Kaiju — nunca emite un comando Sacrificio, nunca marca un objetivo/amenaza, y no cachea D/P entre frames. + +**AC-C36 — Combate no computa `T`** **[Logic/unit]** +Dado el payload de muerte de un héroe, cuando Combate expone D y P, entonces el payload NO incluye ningún campo `T`/`kaiju_timer_remaining_ratio` — ese input lo provee el Temporizador y lo lee Héroes. + +### D. Edge Cases + +**AC-C37 — Loud-fail: `attack_damage < 1`** **[Logic/unit]** +Dado un `CombatProfile` con `attack_damage=0` o negativo, cuando carga, entonces falla ruidosamente. + +**AC-C38 — Loud-fail: `attack_cooldown_s < 0.1`** **[Logic/unit]** +Dado un `CombatProfile` con `attack_cooldown_s < 0.1`, cuando carga, entonces falla ruidosamente (evita DPS ÷0). + +**AC-C39 — Loud-fail: valores negativos en cualquier campo** **[Logic/unit]** +Dado un `CombatProfile` con cualquier campo negativo, cuando carga, entonces falla ruidosamente sin excepción. + +**AC-C40 — `apply_damage` rechazado sobre `DEAD`/`DYING`** **[Logic/unit]** +Dado una unidad en `DEAD` o un héroe en `DYING`, cuando se invoca `apply_damage` sobre ella, entonces es no-op — no decrece vida ni re-dispara notificación de muerte. + +**AC-C41 — Sin fuego amigo** **[Logic/unit]** +Dado dos unidades de la misma facción, cuando una intenta atacar a la otra, entonces `apply_damage` se rechaza — solo entre facciones opuestas. + +**AC-C42 — Mutual kill simultáneo** **(mitad final bloqueada — serialización de beats de Permadeath)** **[Integration]** +Dado dos combatientes cuyos golpes letales se resuelven en el mismo frame, cuando ambos `apply_damage` se procesan, entonces ambos mueren — si ambos son héroes, ambos entran a `DYING` y Combate solo aplica ambos daños; la serialización de beats es de Permadeath (ver AC-C52 y Héroes AC-H25). + +**AC-C43 — Cooldown persiste tras target-switch (anti-exploit)** **[Logic/unit]** +Dado un atacante con cooldown a medio cargar cuyo objetivo muere o se reordena, cuando adquiere un nuevo objetivo, entonces el cooldown en curso sigue corriendo — cambiar de objetivo nunca lo resetea. + +**AC-C43b — Piso de adquisición cierra el exploit de banking de cooldown** **[Logic/unit]** +Dado un atacante cuyo cooldown heredado llega a 0 mientras está en `NO_TARGET` (sin objetivo), cuando adquiere un nuevo objetivo, entonces el primer golpe NO cae instantáneamente — se aplica el piso de adquisición completo (`attack_cooldown_s` desde la adquisición, Regla 3/AC-C05) sin importar que el cooldown heredado ya estuviera en 0. Cierra el exploit de "esperar en `NO_TARGET` hasta que expire el cooldown, luego adquirir para un golpe gratis". + +**AC-C44 — Objetivo sale de `attack_range` a mitad de cooldown: persigue, cooldown no se resetea** **[Logic/unit]** +Dado un atacante en `ATTACKING` cuyo objetivo sale de `attack_range`, cuando lo persigue dentro de `leash_range` y reentra, entonces reanuda con el mismo cooldown en curso; si el objetivo escapa `leash_range`, vuelve a retención y a `NO_TARGET`. + +**AC-C45 — Orden attack-target cuyo objetivo muere: se consume, revierte a auto-aggro** **[Integration]** +Dado una unidad con orden attack-target activa cuyo objetivo muere, cuando se procesa la muerte, entonces la orden se consume y la unidad revierte a auto-aggro — a diferencia de attack-move, que continúa hacia su destino. + +**AC-C46 — `D=0, P=0` se reporta sin juzgar** **[Logic/unit]** +Dado un héroe que muere sin comando Sacrificio ni marcado, cuando Combate computa la clasificación, entonces reporta `D=0, P=0` sin evaluarlo (el Tier 1 resultante es de H4 de Héroes, no de este sistema). + +**AC-C47 — El kaiju llegando a 0 no declara victoria** **[Logic/unit]** +Dado el kaiju cuya vida cruza 0, cuando Combate procesa la muerte, entonces notifica el cruce pero no declara victoria ni emite señal de fin de era. + +**AC-C48 — Sin line-of-sight en el MVP** **[Logic/unit]** +Dado un atacante ranged con un enemigo dentro de `attack_range` pero detrás de un obstáculo, cuando se evalúa si puede atacar, entonces sí puede — `attack_range` es distancia pura, sin LoS en el MVP. + +**AC-C52 — Cooldowns siguen la escala de tiempo de juego (pausa durante `DEATH_HOLD`)** **(desbloqueado — ADR-0002, Accepted 2026-08-21)** **[Integration]** +*Contrato real (ADR-0002): el dueño del flag es el servicio Core **TimeControl**, no Permadeath. Permadeath solicita el régimen `PAUSED`; Combate lee `TimeControl.game_time_paused` (y en general avanza sus cooldowns por `TimeControl.game_tick`/`time_scale`), no lo decide.* +Dado un beat `DYING` con `TimeControl.game_time_paused=true`, cuando transcurre tiempo real, entonces ningún cooldown de combate avanza; si `game_time_paused=false`, los cooldowns continúan a `time_scale` (normal o `SLOWED` bajo una oferta de Reliquias). + +### E. Cross-system: el hand-off D/P a Héroes + +**AC-C50 — Hand-off D/P en el frame de cruce es testeable HOY (Héroes ya diseñado)** **[Integration]** +Dado D/P como valores boolean opacos provistos por Combate (sin importar su fuente — separado del Grupo C, que sí está bloqueado), cuando un héroe cruza 0, entonces Héroes los recibe en ese mismo frame y los usa junto con `T` (leído del Temporizador) para calcular `relic_quality` — mismo patrón de cross-check que AC-T12, sin mock porque el lado receptor (Héroes) ya está Approved. La corrección de D/P en sí sigue bloqueada (AC-C28–C35). + +**AC-C51 — CU-3: tasa de daño entrante cruda, sin proyección** **[Logic/unit]** +Dado una unidad que recibe golpes en una ventana reciente, cuando se consulta la tasa de daño entrante que Combate expone (CU-3), entonces el valor refleja el daño real aplicado — Combate expone el dato crudo; NO computa `time_to_death` (eso es de Encuentro con Kaiju; la tensión de la ventana de ~30s es un contrato conjunto Kaiju/Héroes). + +**AC-C-NEG — Control negativo Pilar 1: tropa sin datos vs. héroe con D/P, en la misma corrida** **[Logic/unit]** — **BLOQUEANTE, crítico de Pilar 1 (mirror de Tropas AC-06)** +Dado, en la misma corrida de test: (a) una tropa que cruza 0 sin comando/marcado, y (b) un héroe que cruza 0 en condiciones equivalentes mockeadas; cuando ambas transiciones se procesan, entonces (a) la tropa transiciona a `DEAD` sin ningún campo D/P/circunstancia, mientras que (b) el héroe transiciona a `DYING` y Combate SÍ computa y expone D/P (aunque resulten `D=0, P=0`) — probando que la distinción de la Regla 7 se hace cumplir activamente en el mismo binario, no por omisión. *Test de regresión: un refactor que fusione los caminos de muerte tropa/héroe es la forma más plausible de romper el Pilar 1, y ese riesgo vive en `apply_damage`.* + +### Clasificación de tipo de story (para el gate de evidencia) + +| AC # | Tipo | Evidencia requerida | Nivel de gate | +|---|---|---|---| +| AC-C01–C07, C07b, C10, C13, C14, C16, C17–C27, C27b | Logic | Unit test (`tests/unit/combat/`) | Bloqueante | +| AC-C08, C09, C11, C12 | Integration | Integration test | Bloqueante | +| AC-C28–C34 | Logic (bloqueado) | Unit test con mock de contrato | Bloqueante (hoy solo vía mock) | +| AC-C35, C52 | Integration (bloqueado) | Integration test con mock (Control+Kaiju / Permadeath) | Bloqueante (hoy solo vía mock) | +| AC-C36–C41, C43, C43b, C44, C46–C48, C51 | Logic | Unit test | Bloqueante | +| AC-C42, C45 | Integration | Integration test (C42 mitad bloqueada) | Bloqueante | +| AC-C50 | Integration | Integration test (Héroes ya diseñado, sin mock) | Bloqueante | +| AC-C-NEG | Logic | Unit test — **excepción de gating**: bloqueante/crítico de Pilar 1, debe correr en cualquier PR que toque `apply_damage` o los caminos de muerte | Bloqueante | + +### Huecos/bloqueados marcados (no diferidos silenciosamente) + +1. **AC-C28–C31, C35 (lado D)** — dependen de que `control-y-seleccion-de-unidades.md` defina el comando Sacrificio (verbo, confirm-step, estado activo/cancelado). Control tiene GDD pero esa porción no está escrita. Ver OQ-2. +2. **AC-C32–C34 (lado P)** — ✅ desbloqueados: Encuentro con Kaiju Approved (`encuentro-con-kaiju.md`) define el marcado (Regla 5/AC-K43/K44); verificables sin mock contra el contrato real. **AC-C35 (lado P)** desbloqueado; su lado D sigue dependiendo del comando Sacrificio de Control (OQ-2). +3. **AC-C42 (mitad final), C52** — dependen de Permadeath (sin GDD): serialización de beats simultáneos y pausa de cooldowns durante `DEATH_HOLD` (mismo hueco que Héroes AC-H11/AC-H25). +4. **AC-C51 / CU-3** — Combate expone la tasa cruda; la ventana de ~30s sigue siendo un contrato conjunto Kaiju/Héroes (ver Héroes AC-H29 y Open Questions). + +## Open Questions + +| # | Pregunta | Owner | Resolución objetivo | +|---|---|---|---| +| OQ-1 | Dónde se almacena el `CombatProfile` — embebido en `TroopDefinition`/`HeroDefinition` vs. resource paralelo referenciado. Decisión de arquitectura, no de diseño. **Nota (2026-08-21):** distinta de la capa de modificadores **por instancia** (`CombatState`), que ya la resolvió ADR-0001 (Accepted); OQ-1 es solo sobre dónde vive el `CombatProfile` **por tipo**. Sigue abierta | technical-director | ADR antes de implementar Combate | +| OQ-2 | El **comando Sacrificio** (verbo + confirm-step) y los verbos **attack-target/attack-move** no están definidos en `control-y-seleccion-de-unidades.md`. El lado `D` de la clasificación (AC-C28–C31) los necesita. ¿Se añaden a Control/órdenes o se especifican aquí? Alinear con Héroes OQ-2 | game-designer | Al revisar Control o al diseñar el sistema de órdenes | +| OQ-3 | ~~Convención de cooldown: el primer golpe cae tras un cooldown completo (F3, actual) vs. "listo-al-adquirir". Afecta cada TTK en un cooldown~~ ✅ **RESUELTO 2026-08-09**: confirmado por godot-specialist en `/design-review` — no precargado (F3), combate resuelto en `_physics_process` con resolución en dos fases (Regla 1) y piso de adquisición (Regla 3/AC-C05/AC-C43b) para cerrar el exploit de banking de cooldown | godot-specialist | ✅ Cerrado | +| OQ-4 | ✅ **RESUELTO 2026-08-21 (ADR-0002, Accepted).** El beat `DEATH_HOLD` pausa el tiempo de juego vía el servicio Core TimeControl: Permadeath solicita `PAUSED`, Combate lee `TimeControl.game_time_paused` y sus cooldowns se congelan (AC-C52). | — | Cerrado | +| OQ-5 | ~~**Tensión de la ventana de ~30s**: el kaiju mata a un héroe en 18–22.5s, menos que los ~30s que exige Héroes AC-H29~~ ✅ **RESUELTO 2026-08-12**: Encuentro con Kaiju (Approved) define `time_to_death = telegraph_duration_s + kill_time_hits×attack_cooldown_s` (F2) — proyección pre-contacto desde el inicio del telegrafío, no desde el primer golpe. Con `telegraph_duration_s=14.0` da 32.0s (Vanguardia) / 36.5s (Guardián), sostiene AC-H29 sin tocar el daño del kaiju | systems-designer / game-designer | ✅ Cerrado (Encuentro F2/AC-K19–K22) | +| OQ-6 | Armadura/mitigación y line-of-sight son hooks post-MVP (el MVP usa daño plano y sin LoS por legibilidad). Revisitar si el daño plano se siente demasiado swingy o si ranged sin LoS se siente mal | systems-designer | Playtest del MVP | +| OQ-7 | ~~El `leash_range` del kaiju es un valor centinela — la IA de Encuentro con Kaiju gobierna la persecución y puede sobreescribir ese campo~~ ✅ **RESUELTO 2026-08-12**: Encuentro con Kaiju (Approved) Regla 2 **anula el campo por completo** (persecución ilimitada durante `CLIMAX`, nunca se lee — AC-K05a/b). La exención de F5 deja de ser "mientras sea centinela": el campo es no-leído por decisión final | game-designer | ✅ Cerrado (Encuentro Regla 2) | +| OQ-8 | ~~¿Quién evalúa victoria/derrota de era? Combate solo notifica muertes (Regla 9)~~ ✅ **RESUELTO 2026-08-12**: Encuentro con Kaiju (Approved) es el dueño — declara victoria al cruzar 0 la vida del kaiju y derrota al morir el último héroe vivo (Regla 9/AC-K49–K53). Combate sigue solo notificando muertes | game-designer | ✅ Cerrado (Encuentro Regla 9) | diff --git a/design/gdd/control-y-seleccion-de-unidades.md b/design/gdd/control-y-seleccion-de-unidades.md new file mode 100644 index 0000000000..dd5e46f5bd --- /dev/null +++ b/design/gdd/control-y-seleccion-de-unidades.md @@ -0,0 +1,312 @@ +# Control y Selección de Unidades + +> **Status**: In Design +> **Author**: usuario + agentes +> **Last Updated**: 2026-07-23 +> **Implements Pillar**: Preparación Ritual, Clímax Explosivo (Pilar 3); soporte a Sacrificio con Peso (Pilar 1) + +## Overview + +El sistema de Control y Selección de Unidades es la capa que traduce las intenciones crudas del jugador (emitidas por el Input) en acciones concretas sobre el mundo: resuelve qué unidad recibe un clic, qué unidades caen dentro de un rectángulo de selección, adónde se dirige una orden de movimiento, y controla la cámara RTS (paneo y zoom). Toma las señales abstractas del Input (`select_requested`, `box_select_requested`, `move_commanded`, `pan_requested`, `zoom_requested`) y les da sentido espacial usando la escala del mundo (`world_unit_scale`=48px/tile). Es el punto de contacto táctil entre el jugador y su ejército: durante la Preparación, es cómo el jugador posiciona deliberadamente sus fuerzas (Pilar 3); durante el Clímax, es cómo comanda con precisión bajo presión. Su responsabilidad hacia el Pilar 1 es que la selección nunca sea ambigua — un héroe jamás debe recibir una orden fatal porque el jugador creyó haber seleccionado a otra unidad. + +## Player Fantasy + +La fantasía de este sistema es la de un comandante cuyo ejército responde con precisión — la sensación de que tu mano y tu intención son una sola cosa con las unidades en pantalla. Su capa directa se siente en cada clic de selección que aterriza en la unidad correcta, en cada rectángulo de arrastre que reúne exactamente al grupo que querías, en cada paneo de cámara que se detiene donde esperabas. Referencias como Warcraft 3 y StarCraft nailean esto: la selección se siente *instantánea y sin ambigüedad*, y la cámara es una extensión invisible de la atención del jugador. Su capa de infraestructura — resolver colisiones de punto, calcular qué cae en un rectángulo, transformar coordenadas de pantalla a mundo — es invisible cuando funciona. Pero la promesa emocional más importante es la del Pilar 1: en un juego donde la muerte es permanente, la confianza en el control debe ser absoluta. El jugador debe poder decir "yo elegí esto" y nunca "el juego seleccionó la unidad equivocada". La precisión aquí no es solo comodidad — es la base sobre la que descansa el peso de cada sacrificio. + +## Detailed Design + +### Core Rules + +1. **Resolución de selección con desempate por prioridad**: al recibir `select_requested(screen_pos)` del Input, el sistema convierte `screen_pos` a coordenadas de mundo y busca unidades bajo/cerca del punto. Regla de desempate en orden estricto: (a) si hay uno o más héroes dentro del radio de selección, se selecciona el **héroe más cercano al punto de clic** (dentro de la misma prioridad de tipo, gana la distancia); (b) si no hay héroe pero hay tropas, se selecciona la tropa más cercana al punto; (c) si nada está dentro del radio, se deselecciona todo (clic en terreno vacío). Esta prioridad héroe>tropa protege el Pilar 1 — un clic ambiguo nunca prioriza una tropa sobre un héroe cercano. +2. **Box-select filtra por tipo**: al recibir `box_select_requested(rect)`, se seleccionan todas las unidades del jugador cuyo centro cae dentro del rectángulo (borde **inclusivo** — un centro exactamente sobre el borde cuenta como dentro). Si el rectángulo contiene tanto héroes como tropas, se seleccionan ambos (el arrastre es una intención explícita de "todo lo que está aquí", a diferencia del clic simple). +3. **Modelo de comando directo (estilo Warcraft 3)**: con unidades seleccionadas, `move_commanded(screen_pos)` envía a todas las unidades seleccionadas hacia esa posición de mundo. El sistema no valida el pathfinding en sí (eso pertenece al movimiento de unidades) — solo resuelve el destino y notifica a las unidades seleccionadas. +4. **Selección solo de unidades propias**: el jugador solo puede seleccionar sus propias unidades (héroes y tropas). Clic sobre un kaiju/esbirro no los selecciona — puede emitir una intención de "objetivo" para un comando de ataque, pero nunca los agrega al conjunto de selección del jugador. +5. **Control de cámara RTS**: el sistema posee la `Camera2D`. Consume `pan_requested(delta)` y `zoom_requested(delta)` del Input (solo activos en `RTS_COMMAND`), aplicando `camera_pan_speed`=30 world units/sec y clamp de zoom a `[zoom_min, zoom_max]` = `[0.5, 1.5]`. La cámara está acotada a los límites del mapa de la era activa (no se puede panear infinitamente al vacío). +6. **Cursor virtual para gamepad**: en modo gamepad (detectado vía `active_input_method` del Input), se muestra un cursor virtual movido por el stick analógico, con snap suave (magnetismo) hacia la unidad seleccionable más cercana. Esto preserva la metáfora espacial de selección del mouse sin forzar un remapeo directo. El cursor virtual solo existe en modo gamepad; se oculta al detectar mouse. + +### States and Transitions + +| Estado | Descripción | Transición desde | Transición hacia | +|---|---|---|---| +| `NONE_SELECTED` | Ninguna unidad seleccionada | Inicio, deselección, muerte de la última unidad seleccionada | `SINGLE_SELECTED`, `MULTI_SELECTED` | +| `SINGLE_SELECTED` | Exactamente una unidad seleccionada | Clic sobre una unidad | `NONE_SELECTED`, `MULTI_SELECTED`, `SINGLE_SELECTED` (otra unidad) | +| `MULTI_SELECTED` | Dos o más unidades seleccionadas | Box-select con 2+ unidades | `NONE_SELECTED`, `SINGLE_SELECTED`, `MULTI_SELECTED` (otro box) | + +- La selección se limpia automáticamente si todas las unidades seleccionadas mueren o dejan de existir (ver Edge Cases). +- El control de cámara es ortogonal a estos estados — funciona en cualquiera de los tres. + +### Interactions with Other Systems + +| Sistema | Dirección | Interfaz | +|---|---|---| +| Input | Input → este sistema | Consume `select_requested(screen_pos)`, `box_select_requested(rect)`, `move_commanded(screen_pos)`, `pan_requested(delta)`, `zoom_requested(delta)` | +| Sistema de Tropas | bidireccional | Consulta posiciones/hitboxes de tropas para resolver selección; notifica órdenes de movimiento a las tropas seleccionadas | +| Sistema de Héroes | bidireccional | Consulta posiciones/hitboxes de héroes (con prioridad de desempate); notifica órdenes a los héroes seleccionados | +| Datos de Era/Civilización | consumidor | Lee los límites del mapa de la era activa (para acotar la cámara) y `world_unit_scale` para transformar pantalla↔mundo | +| Combate/Daño | este sistema → Combate | Provee el conjunto de unidades seleccionadas y el objetivo de un comando de ataque (clic sobre enemigo) | +| UI/HUD | este sistema → UI | Expone el conjunto de selección actual para que la UI muestre indicadores de selección (art bible Sección 3.3, contorno Acero Violeta-Ceniza) | + +*(Nota: Sistema de Tropas y Sistema de Héroes ya tienen GDD y sus interfaces están confirmadas. Combate/Daño y UI/HUD aún no tienen GDD — esas dos interfaces siguen siendo contratos propuestos. La interfaz con Input está confirmada bidireccionalmente contra `design/gdd/input.md`.)* + +## Formulas + +*(Nota: box-select no necesita fórmula — es un test punto-en-rectángulo trivial tras convertir las esquinas del rect vía la Fórmula 2. Pertenece a las reglas de implementación, no aquí.)* + +### Fórmula 1 — Radio de Selección (`select_radius_world`) + +Radio proporcional al tamaño del sprite (no plano), para que la "aura de ambigüedad" del héroe quede acotada a cuánto ocupa realmente en pantalla — clave para el Pilar 1. + +`select_radius_world = unit_visual_radius_world + (click_tolerance_px × zoom / world_unit_scale)` + +| Variable | Símbolo | Tipo | Rango | Descripción | +|---|---|---|---|---| +| Radio visual de unidad | `unit_visual_radius_world` | float | tropa ≈ 0.33–0.5, héroe ≈ 0.5–1.0 (world units) | Mitad del diámetro clicable del sprite en world units. Propiedad per-tipo de las futuras GDDs de Tropas/Héroes (`sprite_diameter_px / world_unit_scale / 2`) | +| Tolerancia de clic | `click_tolerance_px` | float | 4–10 (tuning knob de este sistema) | Perdón plano en píxeles de pantalla de referencia, uniforme a todo tipo de unidad | +| Zoom | `zoom` | float | [0.5, 1.5] | Zoom de cámara actual (registrado) | +| Escala de mundo | `world_unit_scale` | int | 48 (constante registrada) | px por world unit a zoom 1.0 | +| **Radio de selección** | `select_radius_world` | float | ≈ 0.35–1.2 (world units) | Radio en el que un clic se considera "sobre" esta unidad | + +**Ejemplo**: héroe de 64px → `64/48/2 = 0.667`. Con `click_tolerance_px=6`, `zoom=1.0` → término de tolerancia `6×1.0/48 = 0.125`. `select_radius_world = 0.792` world units (≈38px a zoom 1.0). Una tropa de 40px con el mismo setup: `0.417 + 0.125 = 0.542` (≈26px). + +La conversión usa `zoom / world_unit_scale` para mantener el perdón de clic constante en píxeles de pantalla a cualquier zoom (más precisión al acercar, más world-units de tolerancia al alejar), mientras el radio visual de la unidad se mantiene constante en el espacio de mundo. + +### Fórmula 2 — Transformación Pantalla→Mundo (`world_pos`) + +**Crítico para la corrección del Pilar 1.** El `zoom` de `Camera2D` en Godot es **inverso** a la magnificación: valor de zoom menor = más acercado (más px por world unit), valor mayor = más alejado. Esto coincide con `zoom_min=0.5` documentado como "acercamiento táctico". Invertir esto es un bug clásico que rompería silenciosamente toda resolución clic→unidad. + +`world_pos = camera_position_world + (screen_pos_px − viewport_size_px / 2) × (zoom / world_unit_scale)` + +| Variable | Símbolo | Tipo | Rango | Descripción | +|---|---|---|---|---| +| Posición de pantalla | `screen_pos_px` | Vector2(px) | [0, ancho]×[0, alto] | Posición cruda de clic/cursor en px de viewport | +| Tamaño de viewport | `viewport_size_px` | Vector2(px) | dependiente de resolución (ej. 1920×1080) | Dimensiones del viewport actual | +| Posición de cámara | `camera_position_world` | Vector2(world units) | acotada a límites del mapa de la era (Regla 5) | Centro de cámara en espacio de mundo | +| Zoom | `zoom` | float | [0.5, 1.5] | Zoom de cámara actual (registrado) | +| Escala de mundo | `world_unit_scale` | int | 48 (registrado) | px por world unit a zoom 1.0 | +| **Posición de mundo** | `world_pos` | Vector2(world units) | acotada a límites del mapa | Equivalente en mundo del clic de pantalla | + +**Ejemplo**: viewport 1920×1080 (centro 960,540), clic en `(1200, 540)` (240px a la derecha del centro), `camera_position_world=(50,50)`, `zoom=1.0`. `world_units_per_px = 1.0/48 = 0.02083`. Offset `= (5.0, 0)`. `world_pos = (55.0, 50.0)` — coincide con `240px / 48 = 5 world units`. A `zoom=0.5`, el mismo offset resuelve a `240×0.5/48 = 2.5` world units (acercado → misma distancia de pantalla cubre menos mundo, correcto). + +La inversa (`px_per_world_unit = world_unit_scale / zoom`) la necesitarán UI/HUD, minimapa y VFX para colocar marcadores sobre unidades. + +### Fórmula 3 — Snap Magnético del Cursor Virtual de Gamepad + +El pull nunca excede una fracción acotada del input crudo del stick (el jugador siempre puede salir de un "pozo"), y solo asiste cuando la dirección del stick ya concuerda con la dirección hacia la unidad — así empujar hacia otro lado nunca se siente peleado. Reutiliza el mismo desempate héroe>tropa de la Regla 1 para consistencia entre inputs (Pilar 1). + +``` +alignment = max(dot(stick_input_dir, to_unit_dir), 0) +falloff = clamp(1 − distance / snap_radius, 0, 1) +cursor_velocity_world = stick_input_dir × stick_input_mag × virtual_cursor_speed + + to_unit_dir × snap_strength × virtual_cursor_speed × falloff × alignment +``` + +| Variable | Símbolo | Tipo | Rango | Descripción | +|---|---|---|---|---| +| Dirección de stick | `stick_input_dir` | Vector2 (norm) | vector unitario o cero | Dirección del input crudo tras zona muerta (`stick_dead_zone`=0.20 de `input.md`) | +| Magnitud de stick | `stick_input_mag` | float | [0, 1] | Magnitud de deflexión tras remap de zona muerta | +| Dirección a unidad | `to_unit_dir` | Vector2 (norm) | vector unitario | Dirección del cursor a la unidad seleccionable más cercana (desempate héroe>tropa) | +| Distancia | `distance` | float | ≥ 0 (world units) | Distancia del cursor a esa unidad | +| Radio de snap | `snap_radius` | float | 0.6–1.5 (world units), default 1.0 | Radio en el que el magnetismo se activa | +| Fuerza de snap | `snap_strength` | float | 0.25–0.55, default 0.4 | Tope del pull como fracción de `virtual_cursor_speed` | +| Velocidad de cursor | `virtual_cursor_speed` | float | 18–26 world units/sec, default 22 | Velocidad de traslación propia del cursor | +| **Velocidad de cursor** | `cursor_velocity_world` | Vector2(world units/sec) | ≤ `(1+snap_strength) × virtual_cursor_speed` | Velocidad del cursor este frame | + +**Ejemplo**: `distance=0.5`, `snap_radius=1.0` → `falloff=0.5`. Stick apuntando exacto a la unidad (`alignment=1`), deflexión completa (`stick_input_mag=1.0`), `virtual_cursor_speed=22`, `snap_strength=0.4`. Componente crudo = `22 units/sec`. Pull = `0.4×22×0.5×1 = 4.4 units/sec`. Total ≈ `26.4 units/sec` hacia la unidad — ~20% de asistencia cuando ya se apunta al objetivo, cayendo a cero al divergir el stick o salir de `snap_radius`. Como `snap_strength ≤ 0.55`, la asistencia nunca aporta más del 55% de la velocidad cruda: el jugador siempre retiene autoridad sobre la dirección final. + +### Fórmula 4 — Paneo de Cámara Compensado por Zoom (Opción 2: compensación completa) + +`effective_pan_speed_world = camera_pan_speed × zoom` + +| Variable | Símbolo | Tipo | Rango | Descripción | +|---|---|---|---|---| +| Velocidad base de paneo | `camera_pan_speed` | float | 30 (constante registrada) | world units/sec a zoom de referencia (owned por Datos de Era/Civilización) | +| Zoom | `zoom` | float | [0.5, 1.5] | Zoom de cámara actual (registrado) | +| **Velocidad efectiva de paneo** | `effective_pan_speed_world` | float | 15–45 (world units/sec) | Velocidad de traslación real aplicada este frame | + +**Decisión**: se elige compensación completa para que la velocidad *percibida en pantalla* sea constante en todos los niveles de zoom (~1440 px/sec), en vez del comportamiento sin compensar que producía un swing de 3x (paneo más rápido justo en el modo táctico acercado, en tensión con el Pilar 3). A `zoom=1.0` (referencia) da exactamente `camera_pan_speed`=30, así que **no contradice el AC 23 del GDD de Input** (que mide a zoom 1.0). Efecto: paneo más lento en world-units al acercar (más control táctico), más rápido al alejar (traversal más ágil) — encaje natural con el Pilar 3. + +**Ejemplo**: a `zoom=0.5` (acercado), `effective = 30×0.5 = 15` world units/sec → `15×96px = 1440 px/sec`. A `zoom=1.5` (alejado), `effective = 30×1.5 = 45` world units/sec → `45×32px = 1440 px/sec`. Velocidad de pantalla idéntica en ambos extremos. + +## Edge Cases + +- **Si una tropa está adyacente a un héroe** (~1 world unit): parte del espacio entre ambos cae dentro del `select_radius_world` del héroe, y un clic ahí resuelve al héroe por el desempate. Esto es *intencional* (Pilar 1, protege contra sub-seleccionar al héroe) — para seleccionar la tropa adyacente, hacer clic fuera del radio del héroe o usar box-select. El `click_tolerance_px` acotado (default 6) mantiene esa aura pequeña. +- **Si el jugador hace box-select sin arrastrar suficiente** (por debajo del `drag_threshold_px`=5 del Input): el Input ya lo resuelve como clic simple antes de llegar aquí — este sistema nunca recibe un `box_select_requested` con un rectángulo degenerado. +- **Si todas las unidades seleccionadas mueren** (ej. permadeath de un héroe seleccionado): la selección se limpia automáticamente a `NONE_SELECTED` en el mismo frame en que la última unidad deja de existir. No queda una "selección fantasma" apuntando a una unidad muerta. +- **Si el jugador ordena movimiento con selección mixta (héroes + tropas)**: todas las unidades seleccionadas reciben la orden hacia el mismo destino. Cada unidad resuelve su propio pathfinding (no es responsabilidad de este sistema); no hay formación impuesta en el MVP. +- **Si el clic de selección ocurre exactamente sobre un kaiju/esbirro** (unidad no seleccionable): no se agrega a la selección (Regla 4). Si había unidades propias seleccionadas, esto puede emitir una intención de "objetivo de ataque" a Combate/Daño; si no había selección, el clic se ignora (no deselecciona). +- **Si varios candidatos caen dentro del `snap_radius` del cursor de gamepad** (multitud): se elige el más cercano por distancia usando la misma prioridad héroe>tropa de la Regla 1 — nunca un peso distinto, para que gamepad y mouse se comporten idénticamente. +- **Si la cámara intenta panear más allá de los límites del mapa de la era**: se hace clamp a los límites (Regla 5) — la cámara nunca muestra el vacío fuera del mapa jugable, en ningún nivel de zoom. +- **Si el jugador hace zoom mientras panea simultáneamente**: ambas operaciones se aplican en el mismo frame de forma independiente; el clamp de zoom `[0.5, 1.5]` y el clamp de límites de mapa se aplican después de ambas. +- **Si se recibe una orden de movimiento con `NONE_SELECTED`** (ningún seleccionado): la orden se ignora silenciosamente — no hay a quién comandar, no es un error. + +## Dependencies + +**Dependencias hacia arriba (upstream — sistemas que este necesita):** + +| Sistema | Tipo | Interfaz | Estado | +|---|---|---|---| +| Input | Dura | Consume `select_requested`, `box_select_requested`, `move_commanded`, `pan_requested`, `zoom_requested`, `active_input_method` | ✅ Diseñado — contrato confirmado bidireccionalmente contra `design/gdd/input.md` | +| Datos de Era/Civilización | Dura | Lee límites del mapa de la era activa (acotar cámara) y `world_unit_scale`=48 (transformar pantalla↔mundo) | ✅ Diseñado | +| Sistema de Tropas | Dura | Consulta posiciones/hitboxes de tropas + `unit_visual_radius_world` (Fórmula 1) | Sin GDD (contrato propuesto) | +| Sistema de Héroes | Dura | Consulta posiciones/hitboxes de héroes + `unit_visual_radius_world` (Fórmula 1) | ✅ Diseñado — contrato confirmado (`design/gdd/sistema-de-heroes.md`, prioridad héroe>tropa y radio ≥0.5) | + +**Dependientes hacia abajo (downstream — sistemas que dependen de este):** + +| Sistema | Tipo | Interfaz | +|---|---|---| +| Combate/Daño | Dura | Recibe el conjunto de unidades seleccionadas y el objetivo de un comando de ataque | +| UI/HUD | Dura | Recibe el conjunto de selección actual para renderizar indicadores (art bible Sección 3.3) | + +**Nota bidireccional**: Sistema de Tropas y Sistema de Héroes ya tienen GDD y confirman su relación con este sistema (respetan la Fórmula 1 `select_radius` y la prioridad héroe>tropa). Falta que Combate/Daño y UI/HUD, cuando se diseñen, listen su relación con este sistema y respeten los contratos de la Sección C. La interfaz con Input y Datos de Era/Civilización ya está confirmada contra sus GDDs existentes. + +## Tuning Knobs + +| Knob | Símbolo | Tipo | Default | Rango seguro | Qué se rompe en extremos | +|---|---|---|---|---|---| +| Tolerancia de clic | `click_tolerance_px` | float | 6 | 4–10 | <4: clics precisos fallan por temblor de mano; >10: infla el aura de ambigüedad del héroe, arriesgando selección errónea (Pilar 1) | +| Fuerza de snap de gamepad | `snap_strength` | float | 0.4 | 0.25–0.55 | <0.25: el snap casi no ayuda, selección de gamepad frustrante; >0.55: el cursor "pelea" al jugador, difícil elegir una unidad específica en multitud | +| Radio de snap de gamepad | `snap_radius` | float | 1.0 (world units) | 0.6–1.5 | <0.6: el magnetismo casi nunca se activa; >1.5: el cursor salta entre unidades lejanas, sensación errática | +| Velocidad de cursor virtual | `virtual_cursor_speed` | float | 22 (world units/sec) | 18–26 | <18: cursor lento/sluggish; >26: difícil de detener con precisión | + +**Enforcement**: los valores fuera de rango se clampan al límite más cercano al cargarse (consistente con el patrón ya establecido en el GDD de Guardado/Persistencia). + +**Constantes consumidas de otros sistemas (no se duplican aquí, fuente de verdad indicada):** +- `camera_pan_speed`=30, `zoom_min`=0.5, `zoom_max`=1.5, `world_unit_scale`=48 → propiedad de **Datos de Era/Civilización** (registrados). Este sistema los aplica (Fórmula 4 compensa el paneo por zoom) pero no los redefine. +- `stick_dead_zone`=0.20, `zoom_step_fraction`=0.10, `drag_threshold_px`=5 → propiedad del **Sistema de Input**. Alimentan la resolución de cursor/zoom/box-select pero se tunean allá. + +## Visual/Audio Requirements + +Este sistema no renderiza el mundo, pero genera dos elementos visuales cuyo diseño detallado pertenece a UI/HUD y al art bible: +- **Rectángulo de box-select**: el contorno de arrastre mientras el jugador selecciona en área (debe seguir el lenguaje diegético del art bible; color Acero Violeta-Ceniza para consistencia con el estado de selección). +- **Feedback de audio de comando**: emitir señales que el sistema de Audio pueda escuchar (confirmación de selección, orden de movimiento) — este sistema provee el disparador, no el sonido. + +## UI Requirements + +- **Indicador de selección**: por cada unidad seleccionada, la UI muestra un contorno (art bible Sección 3.3/4.4 — Acero Violeta-Ceniza en alto valor/baja saturación, solo contorno delgado sin relleno). Este sistema expone el conjunto de selección; UI/HUD lo renderiza. +- **Cursor virtual de gamepad**: en modo gamepad, se muestra un cursor movido por stick con snap magnético (Fórmula 3). Su apariencia visual pertenece al art bible/UI, pero su comportamiento lo posee este sistema. +- **Rectángulo de box-select**: contorno de arrastre en vivo durante la selección en área. + +**📌 UX Flag — Control y Selección de Unidades**: Este sistema tiene requisitos de UI. En Pre-Producción, ejecutar `/ux-design` para especificar el indicador de selección, el cursor virtual de gamepad y el rectángulo de box-select antes de escribir epics. Las stories que referencien esta UI deben citar `design/ux/[pantalla].md`. + +## Acceptance Criteria + +*(Validado por `qa-lead`. Formato GIVEN-WHEN-THEN. Criterios marcados [BLOQUEANTE, Pilar 1] tienen máxima prioridad.)* + +### Core Rules + +**AC-01** [BLOQUEANTE, Pilar 1]. Given un héroe y una o más tropas están ambos dentro del `select_radius_world` del punto de clic, When `select_requested(screen_pos)` se dispara, Then se selecciona el héroe y ninguna tropa, sin importar qué unidad esté geométricamente más cerca del punto. + +**AC-02** [BLOQUEANTE, Pilar 1]. Given ningún héroe está dentro del `select_radius_world` pero una o más tropas sí, When `select_requested` se dispara, Then se selecciona la tropa más cercana al punto. + +**AC-03** [BLOQUEANTE, Pilar 1]. Given dos o más tropas (sin héroe) caen dentro del radio, When `select_requested` se dispara, Then se selecciona la tropa cuyo centro está más cerca del punto de clic, y las demás no. + +**AC-04** [BLOQUEANTE, Pilar 1]. Given ninguna unidad está dentro del radio del punto de clic, When `select_requested` se dispara, Then todas las unidades seleccionadas se deseleccionan y el estado pasa a `NONE_SELECTED`. + +**AC-05** [BLOQUEANTE, Pilar 1, test negativo]. Given un héroe existe pero está fuera del `select_radius_world` del punto, y una tropa está dentro de su propio radio, When `select_requested` se dispara, Then se selecciona la tropa y NO el héroe fuera de rango — confirma que la prioridad aplica solo dentro-del-radio, nunca por búsqueda global héroe-primero. + +**AC-05b** [BLOQUEANTE, Pilar 1, desempate multi-héroe]. Given dos héroes están ambos dentro del `select_radius_world` del punto de clic, When `select_requested` se dispara, Then se selecciona el héroe cuyo centro está más cerca del punto, y el otro no. + +**AC-06** [BLOQUEANTE, Pilar 1]. Given un rectángulo de arrastre que contiene los centros de dos o más tropas y ningún héroe, When `box_select_requested(rect)` se dispara, Then todas las tropas contenidas se seleccionan y el estado pasa a `MULTI_SELECTED`. + +**AC-07** [BLOQUEANTE, Pilar 1]. Given un rectángulo que contiene el centro de al menos un héroe y al menos una tropa, When `box_select_requested` se dispara, Then héroe(s) y tropa(s) se seleccionan juntos (box-select no aplica la exclusividad héroe>tropa del clic). + +**AC-08**. Given el sprite de una unidad se superpone visualmente al borde del rectángulo pero el centro de la unidad cae *fuera*, When `box_select_requested` se dispara, Then esa unidad NO se selecciona. (Un centro exactamente *sobre* el borde SÍ cuenta como dentro — borde inclusivo, Regla 2.) + +**AC-09** [Integration parcialmente bloqueado]. Given una o más unidades seleccionadas, When `move_commanded(screen_pos)` se dispara, Then cada unidad seleccionada recibe una orden hacia el mismo `world_pos` resuelto, y no ocurre validación de pathfinding en este sistema. + +**AC-10** [Integration]. Given el punto de clic resuelve sobre un kaiju o esbirro, When `select_requested` se dispara, Then el kaiju/esbirro NO se agrega al conjunto de selección del jugador, y la selección previa no se ve afectada. + +**AC-11**. Given el contexto `RTS_COMMAND` activo, When `pan_requested(delta)` se dispara a un `zoom` dado, Then la cámara se traslada a `effective_pan_speed_world = camera_pan_speed × zoom` (ver AC-F7/F8/F9). + +**AC-12**. Given un `zoom_requested(delta)` que empujaría el zoom fuera de `[0.5, 1.5]`, When se aplica el zoom, Then el valor resultante se clampa al límite más cercano (0.5 o 1.5), nunca lo excede. + +**AC-13**. Given `active_input_method` cambia de gamepad a mouse, When se detecta el cambio, Then el cursor virtual se oculta inmediatamente; al volver a gamepad, reaparece. + +### State Transitions + +**AC-14**. Given estado `NONE_SELECTED`, When el jugador hace clic sobre exactamente una unidad seleccionable, Then el estado pasa a `SINGLE_SELECTED` con esa unidad. + +**AC-15**. Given estado `NONE_SELECTED`, When el jugador box-selecciona 2+ unidades, Then el estado pasa a `MULTI_SELECTED`. + +**AC-16**. Given estado `SINGLE_SELECTED`, When el jugador box-selecciona un rectángulo con 2+ unidades, Then el estado pasa a `MULTI_SELECTED` con el nuevo resultado reemplazando la selección única previa. + +**AC-17**. Given estado `SINGLE_SELECTED` con unidad A, When el jugador hace clic sobre una unidad seleccionable distinta B, Then el estado permanece `SINGLE_SELECTED` pero el conjunto cambia de {A} a {B} (A se deselecciona). + +**AC-18** [BLOQUEANTE, Pilar 1, Integration parcialmente bloqueado]. Given `SINGLE_SELECTED` o `MULTI_SELECTED` con todas las unidades seleccionadas muriendo/siendo removidas en el mismo frame, When se procesa la muerte/remoción de la última, Then el estado pasa a `NONE_SELECTED` en ese mismo frame, sin referencia obsoleta a una unidad muerta en el conjunto. + +**AC-19**. Given `MULTI_SELECTED` con 3 unidades y solo 1 muere, When se procesa la muerte, Then el estado permanece `MULTI_SELECTED` con las 2 supervivientes aún seleccionadas. + +### Formulas + +**AC-F1**. Given un héroe de sprite 64px con `world_unit_scale=48`, `click_tolerance_px=6`, `zoom=1.0`, When se computa `select_radius_world`, Then el resultado es `0.792` world units (±0.001), i.e. `64/48/2 + 6×1.0/48`. + +**AC-F2**. Given el mismo setup con una tropa de sprite 40px, When se computa, Then el resultado es `0.542` world units (±0.001). + +**AC-F3** [BLOQUEANTE, Pilar 1]. Given viewport `1920×1080`, `camera_position_world=(50,50)`, `zoom=1.0`, clic en `(1200, 540)`, When se computa `world_pos` vía Fórmula 2, Then el resultado es `(55.0, 50.0)` (±0.01) — un offset de 240px resuelve a exactamente 5.0 world units. + +**AC-F4** [BLOQUEANTE, Pilar 1]. Given el mismo setup pero `zoom=0.5`, When se computa, Then el offset resuelve a `2.5` world units (no `10.0`) — prueba que el zoom se aplica como `zoom/world_unit_scale` (magnificación inversa). Es el test de regresión de mayor valor de toda la GDD: un signo invertido aquí rompería silenciosamente toda resolución clic→unidad. + +**AC-F5**. Given `distance=0.5`, `snap_radius=1.0`, `alignment=1.0`, `stick_input_mag=1.0`, `virtual_cursor_speed=22`, `snap_strength=0.4`, When se computa `cursor_velocity_world`, Then la magnitud hacia la unidad es `26.4` world units/sec (±0.05): crudo `22.0` + pull `4.4`. + +**AC-F6** [property test]. Given cualquier combinación de `distance`, `alignment`∈[0,1], `stick_input_mag`∈[0,1], y `snap_strength` en su máximo seguro 0.55, When se computa `cursor_velocity_world` sobre combinaciones con semilla fija, Then el componente de pull nunca excede `0.55 × virtual_cursor_speed` — el jugador siempre retiene autoridad direccional. + +**AC-F7**. Given `camera_pan_speed=30`, `zoom=0.5`, When se computa `effective_pan_speed_world` y se convierte a px/sec (`×96`), Then el resultado es `15` world units/sec = `1440` px/sec. + +**AC-F8**. Given el mismo setup pero `zoom=1.5` (`×32`), When se computa, Then el resultado es `45` world units/sec = `1440` px/sec — velocidad de pantalla idéntica a AC-F7, confirmando compensación completa. + +**AC-F9**. Given `zoom=1.0`, When se computa `effective_pan_speed_world`, Then el resultado es exactamente `30` world units/sec, coincidiendo con el AC 23 del GDD de Input (medido a zoom 1.0) sin contradicción. + +### Edge Cases + +**AC-20**. Given una tropa a ~1 world unit de un héroe, y un clic cae entre ambos pero dentro del `select_radius_world` del héroe, When `select_requested` se dispara, Then se selecciona el héroe (coincide con AC-01) — documentado como intencional; la suite de regresión nunca debe "arreglar" esto cambiando el orden de desempate. + +**AC-21** [valida el contrato del Input, no este sistema]. Given una distancia de arrastre por debajo de `drag_threshold_px=5`, When el gesto completa, Then este sistema recibe un `select_requested` (clic), nunca un `box_select_requested` con rect degenerado. + +**AC-24** [Integration bloqueado]. Given unidades seleccionadas y el clic resuelve sobre un kaiju/esbirro, When `select_requested` se dispara, Then el conjunto de selección no cambia y se emite una intención de objetivo-de-ataque hacia el kaiju/esbirro clicado. + +**AC-25**. Given ninguna unidad seleccionada y el clic resuelve sobre un kaiju/esbirro, When `select_requested` se dispara, Then no se emite intención de ataque ni cambio de selección (el clic se ignora por completo). + +**AC-26**. Given dos o más unidades seleccionables dentro del `snap_radius` del cursor de gamepad, incluyendo al menos un héroe, When corre la resolución de snap, Then se elige el héroe; si solo hay tropas en rango, la más cercana — mismo orden de prioridad que AC-01/AC-02. + +**AC-27**. Given la cámara en el borde de los límites del mapa de la era activa, When un `pan_requested` la movería más allá, Then la posición se clampa exactamente en el límite y no muestra área fuera del mapa jugable, en ningún zoom. + +**AC-28**. Given `pan_requested` y `zoom_requested` disparan en el mismo frame, When ambos se procesan, Then ambos efectos se aplican independientemente primero, y los clamps de zoom `[0.5,1.5]` y de límites de mapa se aplican después — ninguna operación se salta. + +**AC-29**. Given estado `NONE_SELECTED`, When `move_commanded(screen_pos)` se dispara, Then no ocurre error, ninguna unidad se mueve, y no hay cambio de estado. + +### Tuning Knobs (clamping) + +**AC-30**. Given un valor de `click_tolerance_px` de `2` (bajo) o `15` (alto), When se carga, Then se clampa a `4` y `10` respectivamente. + +**AC-31**. Given un valor de `snap_strength` de `0.1` o `0.7`, When se carga, Then se clampa a `0.25` y `0.55` respectivamente. + +**AC-32**. Given un valor de `snap_radius` de `0.3` o `2.0`, When se carga, Then se clampa a `0.6` y `1.5` respectivamente. + +**AC-33**. Given un valor de `virtual_cursor_speed` de `10` o `30`, When se carga, Then se clampa a `18` y `26` respectivamente. + +### Clasificación de tipo de story (para el gate de evidencia) + +| AC # | Tipo | Evidencia | Gate | +|---|---|---|---| +| AC-01 a AC-08, AC-05b, AC-11 a AC-19, AC-F1 a AC-F9, AC-20, AC-25 a AC-29 | Logic | Unit test automatizado (`tests/unit/selection/`) | Bloqueante | +| AC-09 (recibo de orden), AC-10, AC-18 (integración muerte), AC-24 | Integration | Integration test / playtest documentado | Bloqueante (parcialmente diferido) | +| AC-21 | Logic | Pertenece a la suite de Input, no de este sistema | Advisory | +| AC-30 a AC-33 | Config/Data | Smoke check | Advisory | + +**Nota**: AC-F3/AC-F4 (inversión de zoom en la transformación pantalla→mundo) es el test de regresión de mayor valor de toda la GDD. + +### Huecos/no-testeables marcados + +1. **AC-09/AC-18 (mitad de integración)** bloqueados hasta las GDDs de Sistema de Tropas y Sistema de Héroes — la lógica de este sistema es testeable hoy vía listeners simulados; el cableado real de recibo de orden/evento de muerte no. +2. **AC-24 (emisión de objetivo de ataque)** bloqueado hasta la GDD de Combate/Daño — la emisión es testeable en aislamiento, pero no hay consumidor que verifique el recibo. +3. **AC-F1/AC-F2 (`unit_visual_radius_world`)**: la fórmula es testeable con un escalar simulado; los valores autorales por defecto (héroe 64px, tropa 40px) quedan sin confirmar hasta las GDDs de Tropas/Héroes. +4. **Consumo de la selección por UI/HUD**: fuera del alcance de la suite propia; cuando se diseñe UI/HUD, sus indicadores de selección necesitarán su propio AC referenciando el contrato de conjunto-de-selección de este sistema. + +## Open Questions + +| Pregunta | Owner | Resolución objetivo | +|---|---|---| +| Los valores autorales de `unit_visual_radius_world` (diámetro clicable por tipo de unidad) deben definirse en las GDDs de Sistema de Tropas y Sistema de Héroes; este sistema solo define la fórmula que los consume (Fórmula 1) | game-designer | Al diseñar Tropas/Héroes | +| ¿El MVP incluye grupos de control persistentes (Ctrl+1, etc.)? Se decidió comando directo estilo Warcraft 3 sin grupos para el MVP — revisitar si el playtest muestra que la gestión de selección se siente insuficiente | game-designer | Post-playtest del MVP | +| El flujo de "jugar sin gamepad conectado en PC" (heredado del GDD de Input) interactúa con el cursor virtual — si no hay gamepad, el cursor virtual nunca se muestra; confirmar que esto es suficiente | game-designer / UI-HUD | Antes de implementar input de consola | +| Las interfaces con Sistema de Tropas, Sistema de Héroes, Combate/Daño y UI/HUD son contratos propuestos — confirmar bidireccionalmente cuando cada uno tenga su GDD | game-designer | Al diseñar cada sistema dependiente | diff --git a/design/gdd/datos-de-era-civilizacion.md b/design/gdd/datos-de-era-civilizacion.md new file mode 100644 index 0000000000..b6330a717a --- /dev/null +++ b/design/gdd/datos-de-era-civilizacion.md @@ -0,0 +1,216 @@ +# Datos de Era/Civilización + +> **Status**: In Design +> **Author**: usuario + agentes +> **Last Updated**: 2026-08-16 — `EraDefinition` gana el accesor derivado `get_hero_roster_count()` (nuevo AC-1b) + fila de Interactions/Dependencies para Permadeath; cierra `design/gdd/permadeath.md` OQ-P6 #6. Previo: 2026-08-09 — schema de `EraDefinition` ampliado con `approach_duration_s`/`imminent_window_s` (contrato con Temporizador de Preparación/Ritual confirmado tras su design-review) +> **Implements Pillar**: El Pasado es Poder (Pilar 2); Historia Jugada, No Contada (Pilar 4) + +## Overview + +El sistema de Datos de Era/Civilización es la capa de contenido estático que define cada "capítulo" jugable del juego: qué civilización es, qué roster de tropas y héroes tiene disponibles, qué kaiju la amenaza, y qué vocabulario visual de tileset usa (art bible Sección 6.1). No contiene lógica de juego — es una colección de definiciones tipadas (`Resource` de Godot) que otros sistemas consultan para saber "qué existe en esta era". Cada era también registra qué reliquias de eras anteriores pueden aparecer como props persistentes (Pilar 2), y qué constantes de escala del mundo (`world_unit_scale`, ver Open Questions del GDD de Input) rigen su geometría. Sin este sistema, ningún otro sistema del juego sabría qué unidades, kaiju o entorno cargar para la civilización actual — es el sistema del que depende la mayor cantidad de otros sistemas del MVP. + +## Player Fantasy + +Este sistema no tiene una fantasía propia — su fantasía es prestada de todo lo que hace posible. Cuando el jugador entra a una nueva era y ve un roster de unidades distinto, un kaiju con silueta y comportamiento únicos, y un entorno que reutiliza el vocabulario angular-gótico con acentos de temperatura de color propios (art bible Sección 4.3), está sintiendo el trabajo de este sistema sin saber que existe. Su responsabilidad silenciosa conecta directamente con el Pilar 2 (El Pasado es Poder): es este sistema el que hace posible que una reliquia forjada en la Era 1 aparezca como prop arquitectónico literal en la Era 3 — la sensación de "esto es historia acumulada, no contenido genérico" depende enteramente de que estos datos estén bien estructurados y correctamente vinculados entre eras. + +## Detailed Design + +### Core Rules + +1. **`EraDefinition` como Resource tipado**: cada era es un `Resource` de Godot (`.tres`) con campos: `era_id`, `era_name`, `civilization_name`, `narrative_order` (índice de secuencia), `unit_roster` (referencias a definiciones de tropa/héroe), `kaiju_definition` (referencia), `tileset_vocabulary` (referencia a assets de arte, art bible Sección 6.1), `color_accent` (desplazamiento de Ocre/Piedra por era, art bible Sección 4.3), `legacy_props` (lista de IDs de reliquias de eras anteriores que pueden aparecer como props), `world_unit_scale` (constante que resuelve la pregunta abierta del GDD de Input), `approach_duration_s` (float, segundos totales del acercamiento del kaiju antes del clímax — consumido por Temporizador de Preparación/Ritual F1), `imminent_window_s` (float, duración de la ventana final de aviso `IMMINENT` antes del clímax — consumido por Temporizador de Preparación/Ritual F3), `max_band_size` (int, **tope de tropas simultáneas vivas del jugador** en esta era — dueño de este campo; NO cuenta héroes; consumido por Sistema de Tropas Fórmula E y por el efecto `rally` de Reliquias/Bendiciones vía `band_headroom`). La validación de rango de estos dos últimos (`approach_duration_s > 0`, `0 < imminent_window_s < approach_duration_s`, loud-fail si inválidos) es responsabilidad del Temporizador de Preparación/Ritual al inicializarse (ver ese GDD, Edge Cases/AC-T17/AC-T18) — este sistema solo garantiza que los campos existen con el tipo correcto (float). **Accesor derivado (contrato nuevo con Permadeath, 2026-08-16 — cierra OQ-P6 #6 de `design/gdd/permadeath.md`):** `EraDefinition` expone `get_hero_roster_count() -> int`, calculado leyendo la porción de héroes de `unit_roster` en el momento en que la era alcanza `LOADED` — no es un campo autoral nuevo ni una copia, deriva del mismo dato que Sistema de Héroes ya consume, evitando una segunda fuente de verdad que pudiera desincronizarse del roster real. Es el único dato que Permadeath lee de este sistema, usado exclusivamente para validar la feasibilidad de su cap de bloqueo total (`death_hold_total_lockout_cap_s ≥ N_max × death_hold_min_compressed_s`, Permadeath Regla 8) — no participa de ninguna lógica de contenido de este sistema. **Validación de `max_band_size` (cross-resource, loud-fail al cargar la era — 2026-08-21):** `max_band_size ≥ Σ starting_count_[tipo]` sobre el `unit_roster` (el roster inicial de tropas debe caber en el tope, o la era está rota) — se valida en el mismo camino de carga centralizado que los checks cross-resource de Kaiju (AC-K74/K85). Advisory (warning, no fail, mismo patrón que el tier-3 del Temporizador): `max_band_size + max_concurrent_esbirros` debería quedar dentro del presupuesto de entidades en pantalla (legibilidad/perf — art bible; Kaiju TR/AC-K84). +2. **100% autoral, sin generación procedural**: coincide con el Pilar 4 y el Anti-Pilar del concepto — cada era se define a mano, nunca se genera. La única aleatoriedad del juego vive en Reliquias/Bendiciones, no aquí. +3. **Inmutable en runtime**: las definiciones de era son datos de solo lectura cargados desde Resources. El estado mutable (qué reliquias se ganaron, qué era está activa) vive en Guardado/Persistencia, que referencia estos datos por `era_id`, nunca los duplica. +4. **Props de legado por referencia, no por copia**: `legacy_props` almacena IDs de reliquia, no una copia embebida — evita duplicación de datos y mantiene a Forja de Legado como única fuente de verdad de qué reliquias existen. +5. **Data-driven, nunca hardcodeado**: coincide con el estándar de codificación del proyecto — ningún sistema consumidor debe tener nombres de era, kaiju o unidades escritos en código; todo se resuelve vía referencias a `EraDefinition`. + +### States and Transitions + +| Estado | Descripción | Transición desde | Transición hacia | +|---|---|---|---| +| `UNLOADED` | La era existe como definición en disco, no cargada en memoria | — | `LOADING` (al iniciarse la era o precargarse) | +| `LOADING` | Godot está deserializando el Resource y sus referencias | `UNLOADED` | `LOADED` | +| `LOADED` | Datos en memoria, disponibles para consulta, pero no es la era jugada actualmente | `LOADING`, `ACTIVE` (al salir) | `ACTIVE`, `UNLOADED` (descarga) | +| `ACTIVE` | Es la era actualmente jugada — sus datos alimentan Tropas, Héroes, Kaiju, Temporizador | `LOADED` | `ARCHIVED` (al completarse la era) | +| `ARCHIVED` | La era ya se jugó; sus datos siguen disponibles solo para consulta de legado (props, panteón) | `ACTIVE` | — (permanece archivada) | + +### Interactions with Other Systems + +| Sistema | Dirección | Interfaz | +|---|---|---| +| Sistema de Tropas | Datos → consumidor | Lee `unit_roster` (porción de tropas) y `max_band_size` (tope de tropas simultáneas) de la era `ACTIVE` | +| Sistema de Héroes | Datos → consumidor | Lee `unit_roster` (porción de héroes) de la era `ACTIVE` | +| Encuentro con Kaiju | Datos → consumidor | Lee `kaiju_definition` de la era `ACTIVE` | +| Temporizador de Preparación/Ritual | Datos → consumidor | Lee `approach_duration_s` e `imminent_window_s` de la era `ACTIVE` | +| Transición de Era | bidireccional | Lee `narrative_order` para determinar la siguiente era; marca la era actual como `ARCHIVED` | +| Narrativa Ambiental | Datos → consumidor | Lee `legacy_props` y `tileset_vocabulary` para poblar props de eras anteriores | +| Guardado/Persistencia | bidireccional | Este sistema provee datos inmutables; Guardado/Persistencia almacena qué `era_id` está activa/completada, nunca copia los datos de era en sí | +| Permadeath | Datos → consumidor (solo lectura) | Lee `get_hero_roster_count()` de la era `ACTIVE` sobre la señal era-`LOADED` (no en el `_ready()` de Permadeath) para validar la feasibilidad de su cap de bloqueo total (Regla 8 de `design/gdd/permadeath.md`) — la única lectura cross-system de Permadeath. *(Contrato nuevo, 2026-08-16 — cierra OQ-P6 #6)* | + +*(Nota provisional: Sistema de Tropas, Encuentro con Kaiju, Transición de Era y Narrativa Ambiental aún no tienen GDD — estas interfaces son contratos propuestos. Sistema de Héroes, Temporizador, Guardado/Persistencia y Permadeath ya tienen GDD propio.)* + +## Formulas + +Este sistema no tiene fórmulas de balance de juego, pero es la fuente de verdad de las constantes de escala del mundo que otros sistemas (incluido el GDD de Input) necesitan. + +### `world_unit_scale` +**1 unidad-mundo = 1 tile = 48px a zoom 1.0** + +| Variable | Símbolo | Tipo | Valor | Descripción | +|---|---|---|---|---| +| Escala de unidad-mundo | `world_unit_scale` | int (px/unidad) | 48 (fijo) | Tamaño de tile confirmado (ver Sección 6.1 del art bible) | + +**Justificación**: definir 1 unidad-mundo = 1 tile (en vez de 1 unidad = 1px) mantiene el grid de pathfinding, el footprint de unidades, el espaciado de formaciones y los límites de cámara razonando en el mismo espacio entero, independiente del tamaño de tile en píxeles. A 48px, la tropa más grande (48px) encaja exactamente en una celda; los héroes (48-96px) dan 1-2 tiles limpios. A 32px, la tropa más grande desbordaría la celda en 50%. + +### `camera_pan_speed` (resuelve la pregunta abierta del GDD de Input) +`camera_pan_speed = viewport_width_units / pan_duration_target_s` + +| Variable | Símbolo | Tipo | Valor/Rango | Descripción | +|---|---|---|---|---| +| Ancho de viewport | `viewport_width_px` | int | 1920 (referencia 1080p) | Coincide con la referencia de `drag_threshold_px` del GDD de Input | +| Ancho en unidades | `viewport_width_units` | float (derivado) | 40 | `1920 / 48` | +| Duración objetivo de paneo | `pan_duration_target_s` | float | 1.2–1.5 | Segundos para cruzar el viewport completo a zoom 1.0 | +| **Velocidad de paneo** | `camera_pan_speed` | float | **30** | 27–33 | unidades-mundo/seg | + +**Ejemplo trabajado**: 1920/48 = 40 unidades de ancho a zoom de referencia. Con 30 unidades/seg → 40/30 ≈ 1.33s de cruce (dentro del rango 1.2-1.5s ya fijado en el GDD de Input, y equivalente a 75%/seg, dentro de la regla general de 60-80%/seg). + +### `zoom_min` / `zoom_max` (resuelve la pregunta abierta del GDD de Input) + +Convención: multiplicador estilo Camera2D donde 1.0 = escala nativa; valores <1 acercan (zoom in), valores >1 alejan (zoom out). + +| Variable | Símbolo | Tipo | Valor | Rango seguro | Descripción | +|---|---|---|---|---|---| +| Zoom mínimo (acercamiento táctico) | `zoom_min` | float | 0.5 | 0.4–0.6 | Tropas renderizan al doble de su tamaño nativo — precisión de clic/cursor de gamepad | +| Zoom máximo (encuadre de kaiju) | `zoom_max` | float | 1.5 | 1.4–2.0 | Un kaiju a escala completa (512px) ocupa ~30% de la altura del viewport de referencia, con contexto de batalla visible alrededor | + +**Verificación de pasos**: de 0.5 a 1.5 con `zoom_step_fraction` = 0.10 (ya fijado en el GDD de Input) ≈ 11-12 notches de scroll — granularidad razonable, sin necesidad de re-tunear esa constante. + +*(Nota: `zoom_max` = 2.0 es el extremo superior del rango seguro y cambia el balance hacia vista estratégica general en vez de prominencia del kaiju — marcar para playtest una vez existan sprites de kaiju reales.)* + +## Edge Cases + +- **Si `legacy_props` referencia una reliquia aún no forjada** (el héroe correspondiente sigue vivo): el prop no aparece en absoluto en el entorno de la era nueva. Esto es distinto de la convención de "nichos vacíos" del Salón Conmemorativo (que pertenece a su propio GDD) — aquí, simplemente no se instancia el prop. +- **Si solo existe una era definida** (alcance del MVP): `narrative_order` = 0, sin siguiente era. Transición de Era consulta si existe una era con `narrative_order` superior; si no la encuentra, simplemente no se activa — no es un error, es el estado esperado del MVP. +- **Si un sistema solicita `unit_roster` mientras la era está en `LOADING`**: la solicitud se bloquea/encola hasta que el estado sea `LOADED`. Nunca se retornan datos parciales. +- **Si una referencia a `kaiju_definition` está rota o el Resource no se encuentra**: falla ruidosamente en tiempo de carga/editor (error visible en desarrollo), nunca sustituye silenciosamente un kaiju por defecto — coincide con el Pilar 4 (todo debe ser autoral y específico). +- **Si dos eras distintas referencian el mismo ID de reliquia en `legacy_props`**: permitido — una reliquia puede aparecer como prop persistente en múltiples eras posteriores, ya que el Pilar 2 establece que el legado se acumula, no se consume en una sola aparición. +- **Si `color_accent` no está definido para una era** (arte aún no finalizado en producción): usa como fallback los valores neutrales base del sistema de color (Ocre de Tumba/Piedra Fría, art bible Sección 4.1) en builds de release; en builds de desarrollo puede mostrar un color de placeholder visible para flagging. +- **Si una era `ARCHIVED` descarga sus assets de `tileset_vocabulary` de memoria por rendimiento, y Narrativa Ambiental o el Panteón necesitan consultarla después**: los datos se recargan al estado `LOADED` bajo demanda (no `ACTIVE`) — consultar legado no requiere activación completa de gameplay. + +## Dependencies + +Este sistema no depende de ningún otro (capa Foundation). + +Sistemas que dependen de este (todas duras — no pueden funcionar sin estos datos): + +| Sistema | Tipo | Interfaz | +|---|---|---| +| Sistema de Tropas | Dura | Lee `unit_roster` (porción de tropas) y `max_band_size` (tope de tropas simultáneas — Fórmula E de Tropas, `band_headroom`) | +| Sistema de Héroes | Dura | Lee `unit_roster` (porción de héroes) | +| Temporizador de Preparación/Ritual | Dura | Lee `approach_duration_s` e `imminent_window_s` de la era `ACTIVE` (contrato confirmado — ver `design/gdd/temporizador-de-preparacion-ritual.md`) | +| Encuentro con Kaiju | Dura | Lee `kaiju_definition` | +| Transición de Era | Dura | Lee `narrative_order`; escribe estado `ARCHIVED` | +| Narrativa Ambiental | Dura | Lee `legacy_props` y `tileset_vocabulary` | +| Guardado/Persistencia | Dura (bidireccional) | Almacena qué `era_id` está activa/completada, referenciando estos datos sin copiarlos | +| Permadeath | Dura (solo lectura, load-time) | Lee `get_hero_roster_count()` de la era `ACTIVE`, sobre la señal era-`LOADED`, para la cross-validación de feasibilidad de su cap de bloqueo total (Regla 8) — única lectura cross-system de Permadeath. *(Contrato nuevo, 2026-08-16 — `design/gdd/permadeath.md` está In Design; desbloquea Permadeath AC-P08c)* | +| Forja de Legado | Dura (solo lectura) | Lee qué `era_id` está `ACTIVE` para etiquetar el `origin_era_id` de cada reliquia forjada; también valida unicidad de `hero_id` sobre el roster de héroes de la era `ACTIVE` (Forja AC-FL26). La referencia inversa `EraDefinition.legacy_props → relic_id` (Regla 4) hace la relación bidireccional. *(Contrato confirmado 2026-08-17 — cierra Forja de Legado OQ-FL1; `design/gdd/forja-de-legado.md` Approved)* | +| Reliquias/Bendiciones | Dura (solo lectura, autoría por era) | Lee `invocation_charges` (y posiblemente `draw_size`/`pool_cap`) del `EraDefinition` de la era `ACTIVE` para fijar la economía de invocación por era (Reliquias Regla 5 / AC-RB17), **y el `era_id` de la era ACTIVE** (`active_era_id`) para la exclusión intra-era del pool (Reliquias Regla 10 / F-RB5 — `origin_era_id ≠ active_era_id`). *(Fila recíproca añadida en reconciliación cross-GDD 2026-08-21 — cierra parte de Reliquias OQ-RB3)* | + +*(Bidireccionalidad pendiente de confirmar formalmente cuando cada sistema dependiente tenga su propia GDD — por ahora estas interfaces son contratos propuestos por este documento.)* + +## Tuning Knobs + +| Knob | Símbolo | Tipo | Default | Rango seguro | Qué se rompe en extremos | +|---|---|---|---|---|---| +| Límite de props de legado visibles por era | `max_legacy_props_visible` | int | 10 | 4–20 | <4: el legado se siente invisible/intrascendente, contradice el peso narrativo del Pilar 2; >20: satura la densidad visual incluso en Preparación (que permite máxima densidad según art bible Sección 6.3), compitiendo por atención con la lectura táctica de unidades | +| Tope de tropas simultáneas del jugador | `max_band_size` | int (autoral por era) | — (por era) | `≥ Σ starting_count_[tipo]`; recom. ~1.5–2× el roster inicial de tropas | = roster inicial: `rally` y el reclutamiento de Preparación siempre son no-op (`band_headroom=0`), la atrición se vuelve irrecuperable. Demasiado alto: diluye la escasez de tropas del Pilar 3 y arriesga el presupuesto de entidades en pantalla (legibilidad/perf, junto con `max_concurrent_esbirros`) | + +**Interacción**: este knob no afecta la cantidad *real* de reliquias en `legacy_props` (esas se acumulan sin límite, per Pilar 2) — solo cuántas se renderizan simultáneamente en el entorno de una era dada. Si `legacy_props` excede el límite, el sistema debe priorizar cuáles mostrar (regla de priorización pendiente — ver Open Questions). + +## Visual/Audio Requirements + +Este sistema no tiene requisitos visuales o de audio propios — es una capa de datos pura. Los assets visuales (tileset_vocabulary, definiciones de kaiju/unidades) son referenciados aquí pero especificados por el art bible y los futuros GDDs de los sistemas consumidores (Sistema de Tropas, Sistema de Héroes, Encuentro con Kaiju, Narrativa Ambiental). + +## UI Requirements + +Este sistema no expone UI propia. Los datos que provee (nombre de era, civilización) pueden aparecer en pantallas de otros sistemas (ej. Transición de Era, Salón Conmemorativo), pero el diseño de esas pantallas pertenece a sus GDDs correspondientes. + +## Acceptance Criteria + +*(Validado por `qa-lead`. AC-22 queda explícitamente bloqueado — ver Open Questions.)* + +### Core Rules + +**AC-1** (Config/Data — advisory, smoke check): Given el script `EraDefinition`, When se carga un `.tres` que lo instancia, Then debe exponer todos los campos (era_id, era_name, civilization_name, narrative_order, unit_roster, kaiju_definition, tileset_vocabulary, color_accent, legacy_props, world_unit_scale, approach_duration_s, imminent_window_s) con los tipos declarados correctos, sin error de tipo. + +**AC-1b** (Logic — bloqueante, unit test automatizado): Given un `EraDefinition` cargado en estado `LOADED` con un `unit_roster` de tamaño de héroes conocido, When se invoca `get_hero_roster_count()`, Then retorna exactamente ese conteo, recalculado desde `unit_roster` en cada llamada (no cacheado de forma que pueda desincronizarse) — sin campo autoral separado que pueda contradecir el roster real. *(Contrato nuevo con Permadeath — desbloquea Permadeath AC-P08c; ver `design/gdd/permadeath.md` OQ-P6 #6.)* + +**AC-2** (Config/Data — advisory, smoke check): Given el set de archivos `.tres` de era comprometidos, When se escanea el contenido de era del proyecto, Then no se encuentra ningún dato de era/kaiju/unidad generado en runtime — verificado por grep estático de sistemas consumidores buscando llamadas de generación. + +**AC-3** (Logic — bloqueante, unit test automatizado): Given un `EraDefinition` cargado en estado ACTIVE, When cualquier sistema intenta escribir uno de sus campos en runtime, Then la escritura es rechazada o no tiene efecto persistido, y releer el campo retorna el valor autoral original. + +**AC-4** (Integration — bloqueante, integration test/playtest documentado; BLOQUEADO parcialmente hasta GDD de Guardado/Persistencia): Given un jugador ganó una reliquia ligada a la era X, When el sistema de guardado registra ese estado, Then el registro almacena solo una referencia a `era_id` y al ID de reliquia, y el `EraDefinition` de la era X permanece idéntico byte a byte a su `.tres` autoral. + +**AC-5** (Logic — bloqueante, unit test automatizado): Given una lista `EraDefinition.legacy_props`, When se inspecciona la lista, Then cada entrada es un ID de reliquia (string/enum) y nunca una estructura de datos de reliquia embebida. + +**AC-6** (Integration — bloqueante, diferido hasta GDDs de Sistema de Tropas/Héroes/Encuentro con Kaiju): Given un sistema consumidor, When muestra o referencia contenido de era, Then ningún nombre de era/kaiju/unidad está hardcodeado en su código fuente — verificable por grep una vez esos sistemas existan. + +### States and Transitions + +**AC-7** (Logic — bloqueante): Given una referencia `EraDefinition` en UNLOADED, When se solicita una carga, Then el estado transiciona a LOADING y ningún campo es legible como valor final durante este estado. + +**AC-8** (Logic — bloqueante): Given un `EraDefinition` en LOADING, When el recurso termina de cargar exitosamente, Then el estado transiciona a LOADED y todos los campos están completamente poblados y son legibles. + +**AC-9** (Logic — bloqueante): Given un `EraDefinition` en LOADED, When la era se establece como la era de juego actual, Then el estado transiciona a ACTIVE y exactamente un `EraDefinition` está ACTIVE a la vez (en el caso de una sola era del MVP, trivialmente satisfecho pero debe probarse como invariante). + +**AC-10** (Logic — bloqueante): Given un `EraDefinition` en ACTIVE, When una era distinta se vuelve ACTIVE (o la era se retira explícitamente), Then la era previa transiciona a ARCHIVED y su estado deja de reportarse como ACTIVE. + +**AC-11** (Logic — bloqueante, transición inválida): Given un `EraDefinition` en ARCHIVED, When se intenta una transición directa a ACTIVE sin pasar por una recarga LOADED fresca, Then la transición es rechazada (error o no-op), y el estado permanece ARCHIVED hasta que ocurra primero un ciclo LOADING→LOADED. + +**AC-12** (Logic — bloqueante): Given un `EraDefinition` en ARCHIVED, When se solicita explícitamente una recarga, Then el estado transiciona ARCHIVED → LOADING → LOADED (nunca directo a ACTIVE). + +### Edge Cases + +**AC-13** (Integration — bloqueante, parcialmente diferido hasta el diseño del Salón Conmemorativo): Given la lista `legacy_props` de una era contiene un ID de reliquia cuyo héroe sigue vivo (no forjada), When se consultan los props de legado de la era para renderizado, Then ese ID de reliquia se omite silenciosamente del set retornado/renderizado — sin objeto placeholder en esta capa. + +**AC-14** (Logic — bloqueante): Given el set de eras del MVP contiene exactamente un `EraDefinition` con narrative_order = 0, When Transición de Era verifica si existe una "siguiente era", Then no encuentra ninguna y no se activa, sin lanzar error/excepción. + +**AC-15** (Logic — bloqueante): Given un `EraDefinition` en LOADING, When se solicita `unit_roster`, Then la solicitud se bloquea/encola hasta alcanzar LOADED, y el valor eventualmente retornado nunca es un roster parcial/incompleto. + +**AC-16** (Logic — bloqueante): Given un `EraDefinition` cuya referencia `kaiju_definition` está rota o ausente, When el recurso se carga en un build de desarrollo, Then la carga falla ruidosamente (error/assert visible), y el sistema nunca sustituye silenciosamente un kaiju por defecto. + +**AC-17** (Logic — bloqueante): Given el mismo ID de reliquia aparece en `legacy_props` de la era A y la era B (B narrativamente posterior), When se consultan los props de legado de ambas eras independientemente, Then el ID de reliquia está presente en ambos resultados — confirmando que el legado se acumula en vez de consumirse una sola vez. + +**AC-18** (Config/Data — advisory, smoke check): Given un `EraDefinition` con `color_accent` sin definir, When la era se carga en un build de release, Then el sistema usa como fallback los valores neutrales base (Ocre de Tumba/Piedra Fría) sin error visible; When se carga en un build de desarrollo, Then se muestra un color de placeholder visiblemente distinto en vez del fallback neutral. + +**AC-19** (Logic — bloqueante para la parte de máquina de estados; Integration diferida hasta GDDs de Narrativa Ambiental/Panteón): Given un `EraDefinition` en ARCHIVED con `tileset_vocabulary` descargado, When Narrativa Ambiental o el Panteón consultan los datos de tileset de esa era, Then la era se recarga a estado LOADED (no ACTIVE) y retorna datos de tileset válidos. + +### Tuning Knobs + +**AC-20** (Logic — bloqueante): Given `max_legacy_props_visible` se establece fuera del rango seguro (ej. 2 o 25), When el valor se carga/aplica, Then se hace clamp al rango seguro [4, 20] antes de usarse, y el tamaño real de la lista `legacy_props` nunca se trunca por este límite (solo el subset renderizado/visible se limita). + +**AC-21** (Config/Data — advisory, smoke check): Given no hay override explícito para `max_legacy_props_visible`, When se carga una era, Then el valor efectivo usado para renderizado es 10 (el default documentado). + +### Clasificación de tipo de story (para el gate de evidencia) + +| AC # | Tipo | Evidencia requerida | Nivel de gate | +|---|---|---|---| +| AC-1, AC-2 | Config/Data | Smoke check | Advisory | +| AC-3, AC-5, AC-7 a AC-12, AC-14 a AC-17, AC-19, AC-20 | Logic | Unit test automatizado | Bloqueante | +| AC-4, AC-6, AC-13 | Integration | Integration test / playtest documentado (parcial, diferido) | Bloqueante | +| AC-18, AC-21 | Config/Data | Smoke check | Advisory | + +### Huecos/no-testeables marcados (no escritos como criterios) + +1. **AC-6** — no se puede verificar "sin nombres hardcodeados" en sistemas que aún no existen (Sistema de Tropas, Héroes, Encuentro con Kaiju). Aplicar retroactivamente cuando esos GDDs/implementaciones existan. +2. **AC-4 y AC-19** — el comportamiento de ida y vuelta completo depende de interfaces de Guardado/Persistencia y Narrativa Ambiental/Panteón que aún no existen. El lado de `EraDefinition` es testeable ahora en aislamiento; el camino de consulta/guardado cruzado no. +3. **AC-13** — el comportamiento de renderizado de placeholder del Salón Conmemorativo está fuera de alcance de este GDD; solo la mitad de "omisión en la capa de consulta" es testeable ahora. +4. **AC-22 (regla de prioridad al exceder `max_legacy_props_visible`)** — genuinamente bloqueado, no diferido: no existe una regla contra la cual testear. Debe volver como pregunta de diseño abierta antes de escribir un criterio (ver Open Questions). +5. **world_unit_scale / camera_pan_speed / zoom_min / zoom_max** — son constantes de fórmula ya fijadas, pero pertenecen conceptualmente a un sistema de *cámara*, no a `EraDefinition` en sí. No se escribe AC aquí; se recomienda que el futuro sistema de cámara/Control y Selección de Unidades cargue el AC de timing de paneo (1.2–1.5s) como criterio de Integration/Feel. + +## Open Questions + +| Pregunta | Owner | Resolución objetivo | +|---|---|---| +| ¿Qué regla de prioridad decide qué props de legado mostrar cuando `legacy_props.size()` excede `max_legacy_props_visible`? (bloquea AC-22) | game-designer | Antes de implementar el renderizado de props de legado | +| ¿`world_unit_scale`, `camera_pan_speed`, `zoom_min`/`zoom_max` deberían moverse conceptualmente a un futuro GDD de Cámara/Control y Selección de Unidades en vez de vivir aquí? Se definieron en este documento por necesidad (bloqueaban al GDD de Input), pero pertenecen más a cámara que a datos de era | game-designer | Al diseñar Control y Selección de Unidades | +| Las interfaces bidireccionales con Encuentro con Kaiju, Transición de Era y Narrativa Ambiental son contratos propuestos — deben confirmarse formalmente cuando cada uno tenga su propia GDD. (Sistema de Tropas, Sistema de Héroes y Temporizador de Preparación/Ritual ya tienen GDD propia y sus contratos con este sistema están confirmados — Temporizador confirmado 2026-08-09 con la adición de `approach_duration_s`/`imminent_window_s` al schema.) | game-designer | Al diseñar cada sistema dependiente restante | diff --git a/design/gdd/encuentro-con-kaiju.md b/design/gdd/encuentro-con-kaiju.md new file mode 100644 index 0000000000..e85daa7e0c --- /dev/null +++ b/design/gdd/encuentro-con-kaiju.md @@ -0,0 +1,799 @@ +# Encuentro con Kaiju + +> **Status**: Approved (2026-08-12, tras 3er re-review — 7 agentes + creative-director; 4 bloqueantes resueltos en sesión, aceptado sin 4ª pasada por decisión del usuario). **Parche cross-GDD 2026-08-16**: nueva Regla 11 + AC-K89 — el ciclo de intención del kaiju consume `game_time_paused` de Permadeath y se congela durante `DEATH_HOLD`; cierra `design/gdd/permadeath.md` OQ-P6 #4 / OQ-P1. Aditivo, no reabre el veredicto Approved. +> **Author**: usuario + agentes +> **Last Updated**: 2026-08-16 — previo: 2026-08-12 (3er re-review, 7 agentes + creative-director) — correcciones de coherencia: (1) **loud-fail doctrine reparada** — `assert()` es no-op en builds exportados, reemplazado doc-wide por `push_error()` + halt explícito (godot-specialist); (2) **6 loud-fail configs sin AC** ahora cubiertas (AC-K79–K84, qa-lead); (3) acoplamiento **F4×F2 ahora enforced como loud-fail** al cargar era, ya no prosa (AC-K85, systems-designer); (4) **degeneración de Furia (b) en N=2** cerrada — `required_engaged_heroes` pasa a `floor(fraction×N)+1` (>N/2 en el default), dominio loud-fail estrechado a `[0.34,1.0]`, AC-K63/b/c/d actualizados + AC-K86 (N=2, systems-designer); (5) **`entity_id`** definido como campo estable autoral + corrección de la cita errónea a AC-C06 (AC-K48, ai-programmer); (6) **doble `apply_damage` en el frame de `STRIKING`** cerrado — dueño único del gate en el frame de handoff + abort-tras-cruce-de-0 (Regla 4/AC-K61/K75/K87, ai-programmer); (7) **exención de esbirros en Furia (b)** — `engaged_hero_count` cuenta daño a kaiju **o** esbirros (Regla 10b, game-designer); reencuadre de Player Fantasy: Furia es presión agregada legítima, no un redirect oculto; (8) **canal no-auditivo de tercio 1** (viñeta de borde + rumble bajo el toggle KV-10) para cerrar el hueco de accesibilidad de Pilar 4 (KV-1/KV-10, ux-designer) +> +> _Historial previo — 2026-08-11 (round 1, 9 bloqueantes) y 2026-08-12 (round 2, 6 bloqueantes): ver `design/gdd/reviews/encuentro-con-kaiju-review-log.md`._ +> **Implements Pillar**: Preparación Ritual, Clímax Explosivo (Pilar 3 — sistema climático); Historia Jugada, No Contada (Pilar 4) + +## Overview + +Encuentro con Kaiju es el sistema que da vida al clímax que Temporizador de Preparación/Ritual anuncia y que Combate/Daño resuelve: cuando el reloj de Temporizador dispara `entered_climax`, este sistema instancia al kaiju (y sus esbirros, si la era los define) desde el `kaiju_definition` de Datos de Era/Civilización, dirige su IA de selección de objetivo y comportamiento sobre el ciclo de enganche que Combate/Daño ya posee (`NO_TARGET`/`TARGET_ACQUIRED`/`ATTACKING`), gobierna cuándo y a quién ataca, y **acota la duración del clímax** vía la Furia (enrage) — que además fuerza al jugador a comprometer héroes en la pelea, no solo tropas (Regla 10). A nivel de datos, es la capa de comportamiento — no de daño ni de pacing — que decide qué hace el kaiju en cada instante del clímax: a qué objetivo se engancha, cuándo despliega esbirros, cuándo marca un objetivo/aliado como protegido (vía **defensiva** de la clasificación `P` de Héroes) o una amenaza propia como crítica (vía **ofensiva**), y cuándo proyecta cuánto le queda de vida a un héroe bajo ataque (`time_to_death`) para que la ventana de decisión de ~30s que Héroes exige (AC-H29) sea real y no un choque de matemática instantánea. A nivel de jugador, es el sistema donde vive el **Pilar 3** en su forma más pura: el momento explosivo y breve que toda la Preparación pausada estaba construyendo, y donde el **Pilar 4** se juega su honestidad — el kaiju debe telegrafiar, marcar y amenazar de forma legible, nunca emboscar. Sin este sistema, Temporizador contaría hacia un vacío, Combate/Daño no tendría antagonista al que aplicarle su autoridad de daño, y la clasificación `P` de Héroes (¿la muerte protegió algo?) no tendría nada que marcar. + +## Player Fantasy + +El jugador se enfrenta, por fin, a la amenaza que toda la Preparación anticipaba — y se siente pequeño. El contraste de escala brutal (art bible Principio B) no es solo estética: es la traducción visual de lo que Temporizador ya construyó como tensión de reloj. Donde Preparación era la víspera, el clímax es la batalla misma — pero el jugador no la vive como un enjambre de números; la vive leyendo telegrafíos, decidiendo a qué héroe compromete, a cuál retira, y a qué marca prioriza defender, todo bajo el kaiju que domina la pantalla. + +Este sistema es donde la matemática fría de Combate/Daño se vuelve carne: el mismo kaiju que en una tabla de TTK mata a un Guardián en 22.5s aquí es una silueta que se acerca, un ataque que se ilumina antes de caer, un objetivo marcado que grita "esto importa". La ventana de ~30s que Héroes exige (AC-H29) solo es una decisión real si el jugador puede leerla venir — no es un contador oculto, es el pulso visible del combate. Las palancas tácticas **vivas** del jugador contra el kaiju son dos: **retirar** al objetivo marcado de `aggro_range` para forzar una reselección (Regla 3), o **neutralizar** el ataque telegrafiado infligiendo daño suficiente antes de que caiga (Regla 7). El marcado defensivo/ofensivo (`P`, Regla 5) no *redirige* al kaiju — no cambia a quién golpea — sino que **clasifica** la muerte de un héroe como protectora o neutralizadora para la Forja de Legado (Permadeath): la decisión de interponer un héroe en `guard_radius` o de lanzarlo a neutralizar es una apuesta sobre *qué legado nace de esa muerte* (Pilar 2), leída y puntuada retroactivamente, no una orden que el kaiju obedezca. El "ajedrez" del Pilar 1 se juega en *a quién comprometes contra qué telegrafío, y si lo retiras, lo neutralizas o lo dejas morir con propósito* — no en desviar los golpes del kaiju, que persigue su propia lógica de amenaza objetivo-a-objetivo (F1). + +**Dos capas distintas de control, deliberadamente separadas**: el jugador **no** controla *a quién* golpea el kaiju en un instante dado (eso lo decide la lógica de amenaza de F1, y el marcado `P` no lo redirige). Lo que el compromiso **agregado** del jugador sí modula es *cuánto tiempo el kaiju juega con reglas benignas*: la Furia (Regla 10) es una respuesta legible y anunciada al ritmo global de la pelea — estancarse o esconder al grueso del roster endurece al kaiju para el resto del encuentro. Esto no contradice "el kaiju persigue su propia lógica": la lógica de *targeting* sigue siendo del kaiju; lo que el jugador presiona es el *tempo* de la escalada, no el blanco de cada golpe. La Furia nunca es una emboscada — cuando llega, se telegrafía como cambio de línea base permanente (KV-9/KA-6), y sus disparadores (tiempo y desenganche del roster) son propiedades del comportamiento *sostenido* del jugador, no de una acción puntual oculta. + +Los esbirros (si la era los define) añaden una segunda textura: el caos gestionable de un enjambre menor contra la amenaza singular del kaiju — dos escalas de peligro en la misma pantalla, nunca confundibles entre sí. + +La fantasía se rompe si el kaiju embosca. Cada golpe letal debe telegrafiarse con tiempo suficiente para reaccionar (Pilar 4); cada marca debe ser visible antes de que importe, nunca revelada después del hecho. Un jugador que pierde un héroe sin haber podido leer el peligro no vivió tragedia — vivió una encerrona, y eso traiciona exactamente lo que este juego promete. + +## Detailed Design + +### Core Rules + +1. **Instanciación en `entered_climax`, data-driven**: al recibir la señal `entered_climax` de Temporizador de Preparación/Ritual, este sistema instancia el kaiju de la era `ACTIVE` desde `kaiju_definition` (Datos de Era/Civilización) — su `CombatProfile` completo (`attack_damage`, `attack_range`, `attack_cooldown_s`, `aggro_range`; `leash_range` no aplica, Regla 2) más los datos propios de este sistema (perfil de amenaza, cadencia de esbirros si la era los define). Los esbirros, si existen para esa era, se instancian según su propio calendario de oleadas (Regla 8) — nunca antes de `entered_climax`. + +2. **Kaiju sin leash — persecución ilimitada durante `CLIMAX`** (resuelve Combate/Daño OQ-7 de forma permanente): el `leash_range` del kaiju queda sin efecto — una vez enganchado a un objetivo, el kaiju nunca regresa a una "posición de retención" por distancia recorrida. Solo deja de perseguir un objetivo cuando ese objetivo muere, sale de `aggro_range` sin que haya otro objetivo válido cercano, o este sistema selecciona un nuevo objetivo (Regla 3). Esto retira la excepción nombrada de F5 de Combate/Daño para este campo — no es un valor centinela pendiente, es la decisión final: el kaiju siempre está en la pelea mientras dure `CLIMAX`. + +3. **Selección de objetivo ponderada por amenaza, no por cercanía** (sobreescribe el aggro por defecto de Combate/Daño Regla 4 — **solo para el kaiju**): cuando el kaiju necesita un nuevo objetivo (al completar un ataque o perder el actual), este sistema calcula una puntuación de amenaza por candidato válido (Fórmula F1, ver Formulas) que pondera: (a) daño reciente infligido al kaiju por ese candidato dentro de una ventana de amenaza acumulada, (b) un peso fijo mayor para héroes que para tropas, y (c) una variación aleatoria acotada que evita un patrón 100% determinista explotable. El candidato con mayor puntuación se selecciona. Los esbirros, en cambio, usan el aggro estándar de Combate/Daño (más cercano) sin modificar — la asimetría de comportamiento entre kaiju y esbirros es deliberada (Player Fantasy: dos escalas de peligro, nunca confundibles). + +4. **Ciclo de intención del kaiju: `SEEKING → TELEGRAPHING → STRIKING → SEEKING`** (capa que este sistema añade sobre el FSM de enganche de Combate/Daño, ver States and Transitions): tras seleccionar un objetivo (Regla 3), el kaiju no ataca de inmediato — entra a una fase de telegrafío de `telegraph_duration_s` (tuning knob) antes de que Combate/Daño resuelva el golpe. Esto es **exclusivo del kaiju**; los esbirros atacan con el ciclo estándar de Combate/Daño (Regla 3 de ese GDD) sin telegrafío. + + **Gate de ataque del FSM base (supresión durante `SEEKING`/`TELEGRAPHING`)**: mientras el kaiju está en `SEEKING` o `TELEGRAPHING`, la resolución de ataque del FSM de enganche de Combate/Daño queda **explícitamente suprimida** para el kaiju — aunque esté `TARGET_ACQUIRED`, con el objetivo dentro de `attack_range` y el cooldown listo, el FSM base **no** dispara `apply_damage`. El único golpe que el kaiju entrega en todo el ciclo proviene de la resolución de `STRIKING` (donde este sistema devuelve el control al ciclo estándar, ver States and Transitions). El `attack_cooldown_s` del kaiju no corre durante `SEEKING`/`TELEGRAPHING`; solo empieza a contar al resolverse `STRIKING`. Sin este gate, el kaiju dispararía un ataque "normal" antes o durante su propio telegrafío (Combate/Daño Regla 3 auto-dispara `apply_damage` para cualquier combatiente en rango con cooldown listo) — el gate es la razón por la que el telegrafío es honesto (un solo golpe telegrafiado por ciclo, nunca un golpe extra sin aviso, Pilar 4). + + **Cómo se dispara el golpe de `STRIKING` (override explícito del piso de adquisición)**: la entrada a `STRIKING` **invoca `apply_damage` directamente**, en el mismo frame de la resolución del telegrafío, **saltándose el piso de adquisición y la espera de cooldown completo del FSM base** (Combate/Daño Regla 3/AC-C05/AC-C43b) para esa única llamada. Esto es necesario y deliberado: como el `attack_cooldown_s` del kaiju estuvo suprimido durante `SEEKING`/`TELEGRAPHING` (arriba), está en 0 al entrar a `STRIKING` — y el FSM base, por su piso de adquisición, **no** produciría un golpe inmediato con cooldown en 0 (exigiría un cooldown completo adicional). Sin este override, el golpe telegrafiado se retrasaría un `attack_cooldown_s` completo tras cada telegrafío, rompiendo AC-K10 (el golpe resuelve *al completar* el telegrafío) y F2 (`time_to_death` asume que el impacto cae exactamente al final de `telegraph_duration_s`, no después). El `attack_cooldown_s` del kaiju empieza a contar **después** de que este golpe de `STRIKING` se aplica, gobernando el siguiente ciclo estándar hasta que el kaiju vuelva a `SEEKING`. Este override es exclusivo del golpe de `STRIKING` del kaiju — los esbirros y los golpes rutinarios siguen el piso de adquisición normal de Combate/Daño sin excepción. + +5. **El telegrafío ES el marcado** (resuelve el contrato `P` de Combate/Daño, AC-C32/AC-C33 hoy bloqueadas por mock): al entrar a `TELEGRAPHING`, el objetivo elegido se convierte en `marked_target` (vía **defensiva** de `P` — cualquier héroe que muera protegiéndolo dentro de `guard_radius` satisface `P=1`) y la acción de ataque en sí se convierte en `marked_threat` (vía **ofensiva** de `P` — un héroe que neutraliza el ataque telegrafiado, Regla 7, satisface `P=1` por esa vía). Ambos marcados se limpian al salir de `TELEGRAPHING` — nunca persisten entre telegrafíos sucesivos. + +6. **`time_to_death` se proyecta desde el inicio del telegrafío, no desde el contacto** (resuelve Combate/Daño OQ-5): al entrar a `TELEGRAPHING` contra un héroe, este sistema expone `time_to_death = telegraph_duration_s + (kill_time_hits_kaiju × attack_cooldown_s_kaiju)` para ese héroe (usando F2/F3 de Combate/Daño), consumido por la ventana de decisión de Héroes (AC-H29/U-3). Esto es lo que hace alcanzable la ventana de ~30s de Héroes pese a que el TTK de contacto puro del kaiju sea de 18–22.5s (Combate/Daño F3) — el telegrafío añade el margen que el contacto por sí solo no puede dar. `time_to_death` modela **solo** el daño del kaiju y lleva la etiqueta semántica "vs kaiju" (no es una promesa absoluta de vida — ver Regla 6b y Edge Cases). + +6b. **`incoming_damage_rate_total` — señal de daño entrante de todas las fuentes (cierra el hueco de honestidad de Pilar 4)**: en paralelo a `time_to_death` (que es kaiju-only, Regla 6), este sistema expone por héroe `incoming_damage_rate_total` = la suma de DPS de **todos** los atacantes que golpean a ese héroe en el instante de lectura (el kaiju proyectado + cada esbirro actualmente enganchado a él, Fórmula F6). El consumidor (readout de Héroes U-3 / UI/HUD) **debe** reflejar la fuente más peligrosa: si `hero_HP / incoming_damage_rate_total` es menor que `time_to_death`, la muerte real llega antes de lo que el número kaiju-only sugiere, y el readout no debe presentar `time_to_death` como si fuera la vida garantizada. Sin esta señal, un jugador confiaría en un número que puede estar equivocado en la dirección fatal (muerte por esbirros no descontada) — exactamente la emboscada silenciosa que el Pilar 4 prohíbe. Esta señal es numérica (no texto), coherente con KU-3. Resuelve OQ-3. + +7. **Interrupción de un ataque telegrafiado (neutralización)**: durante `TELEGRAPHING`, si el daño acumulado infligido al kaiju **desde el instante en que empezó ese `TELEGRAPHING`** alcanza `threat_neutralize_damage_threshold` (Fórmula F4), el ataque telegrafiado se cancela — el kaiju no llega a `STRIKING`, entra a `STAGGERED` (`stagger_duration_s`) antes de volver a `SEEKING`. El contador de daño acumulado se **resetea a 0 en cada transición `SEEKING→TELEGRAPHING`** — daño residual de una fase anterior (otro `TELEGRAPHING` contra un objetivo distinto, o daño infligido durante `SEEKING`/`STRIKING`) nunca cuenta hacia la neutralización del telegrafío actual; cierra el mismo tipo de exploit que Combate/Daño ya cerró para el reset de cooldown por cambio de objetivo. El combatiente cuyo golpe cruza el umbral se acredita como quien neutralizó la amenaza marcada — si es un héroe, satisface `P=1` vía ofensiva (Regla 5), incluso si ese mismo golpe resulta en la muerte del héroe por un efecto simultáneo (mutual kill, contrato de Combate/Daño AC-C42). + +8. **Esbirros: `CombatProfile` propio, oleadas por cadencia** (si la era los define, Regla 1): cada tipo de esbirro tiene su propio `CombatProfile` (mismo schema que Combate/Daño, propiedad de este sistema) y se instancia en oleadas durante `CLIMAX` según una cadencia definida en Formulas/Tuning Knobs. Los esbirros usan el ciclo estándar de enganche de Combate/Daño (aggro por cercanía, sin telegrafío) — nunca comparten el ciclo de intención del kaiju (Regla 4). + +8b. **Cota de esbirros concurrentes vivos (`max_concurrent_esbirros`)**: para evitar la acumulación no acotada de esbirros (hasta `max_waves × esbirro_wave_size` unidades vivas simultáneas si el jugador no limpia oleadas — satura la pantalla y erosiona la dominancia visual del kaiju del Pilar 3 / art bible 3.4), el spawn de oleadas respeta un tope `max_concurrent_esbirros` (dato por era). Cuando una oleada está **debida** por cadencia (F3) pero el conteo de esbirros vivos ya alcanzó `max_concurrent_esbirros`, la oleada se **difiere** (no se descarta): el spawner espera hasta que el conteo de vivos baje del tope y entonces instancia la(s) oleada(s) diferida(s), respetando siempre el tope. Esto convierte "limpiar esbirros" en una palanca real del jugador (limpiar abre espacio para la siguiente oleada más pronto; no limpiar la retrasa) sin cambiar la cadencia-objetivo `wave_count(t)` de F3 — solo el momento real de instanciación. `max_concurrent_esbirros=0` en una era sin esbirros es redundante con `max_waves=0` pero válido (cero unidades por ambas vías). + +9. **Evaluación de victoria/derrota de era** (resuelve Combate/Daño Regla 9/OQ-8 y Temporizador OQ-2 — este sistema es el dueño): **Victoria** — el kaiju cruza 0 de vida (notificación de `apply_damage` vía Combate/Daño); este sistema declara la victoria de la era en ese instante. **Derrota** — el último héroe vivo de la civilización activa muere (ningún héroe permanece en un estado `ALIVE_*`); este sistema declara la derrota en ese instante. Las tropas no son condición de derrota — coherente con que los héroes, no las tropas, son la apuesta que Permadeath cuida (Pilar 1); una civilización sin héroes no tiene con qué continuar el ritual, aunque conserve tropas vivas. + +10. **Furia del kaiju (`ENRAGED`) — cota de duración del clímax y anti-turtle** (resuelve Temporizador OQ-4 y cierra el exploit de ganar sin arriesgar héroes): el kaiju entra a un modo `ENRAGED` **irreversible** cuando se cumple **cualquiera** de dos disparadores (Fórmula F5): + - **(a) Por tiempo** — `climax_elapsed_s ≥ enrage_time_threshold_s`. Cota dura del **inicio de la escalada** (no de la duración total del clímax — la Furia sube la presión pero no termina la pelea por sí sola; ver Nota de robustez en F5 y OQ-12): a partir de este umbral la pelea se vuelve progresivamente más letal, empujando hacia la resolución, coherente con Pilar 3 ("si una mecánica alarga el combate sin aumentar la tensión, se recorta"). Responde directamente a Temporizador OQ-4 (¿necesita el `CLIMAX` un timer interno / enrage?): **sí, y vive aquí** — con la cota dura *total* diferida conscientemente a OQ-12 (post-MVP). + - **(b) Por desenganche de héroes — consciente del roster (no basta un solo héroe-cebo)** — la Furia se dispara cuando el número de héroes vivos **comprometidos en la pelea** dentro de la ventana `hero_disengage_window_s` cae por debajo de una fracción requerida del roster vivo (`hero_engagement_fraction`, F5). **Definición de "comprometido" (exención de esbirros)**: un héroe cuenta como comprometido si dañó al kaiju **o a un esbirro** dentro de la ventana — no solo al kaiju. Esto cierra el falso positivo por el que un héroe correctamente dedicado a limpiar una oleada de esbirros (una lectura táctica válida, sobre todo con `max_concurrent_esbirros` saturando el espacio, Regla 8b) no contaba y el jugador comía Furia irreversible por jugar bien. La Furia (b) castiga el *desenganche* de la pelea (parquear héroes lejos de todo peligro, sin tocar kaiju ni esbirros), no la elección de *contra qué* enemigo se compromete el héroe. No basta con que **algún** héroe (uno solo, permanente) esté comprometido cada ventana: el jugador debe mantener **comprometida una fracción de los héroes vivos** dentro de la ventana. Castiga tanto el turtle de parquear todos los héroes fuera de peligro y ganar solo con tropas, **como** la estrategia degenerada de dejar un único héroe-cebo permanente y retirar a todos los demás para siempre (que un disparador "algún héroe" no impedía). Esto ata el núcleo mecánico al Pilar 1 en su forma real — no se puede ganar comprometiendo a *uno* y escondiendo al resto; la decisión de *a quién comprometes cada encuentro* (Player Fantasy) tiene dientes. **Degradación natural con bajas**: la fracción se calcula sobre héroes *vivos*, así que a medida que el roster muere el listón baja proporcionalmente — nunca se le exige al jugador comprometer héroes que ya no tiene (con un solo héroe vivo, la condición degenera a "ese héroe debe estar comprometido cada ventana", que es correcto: no se puede rotar lo que no existe). + + Al entrar a `ENRAGED`, el kaiju escala vía F5: su `telegraph_duration_s` efectivo se multiplica por `enrage_telegraph_factor` (`<1`, telegrafíos más cortos → más presión y menos margen) y su `attack_damage` efectivo por `enrage_damage_factor` (`≥1`). La Furia es **monótona/latcheada** — una vez activada no se revierte aunque un héroe vuelva a enganchar o el reloj siga (evita un yo-yo explotable de entrar/salir de Furia). La escala de daño de la Furia es la **única excepción documentada** a "daño plano, sin multiplicadores" de Combate/Daño Regla 5 (paralela a cómo la Regla 2 es la única excepción al leash) y se telegrafía con el mismo ciclo de intención (Pilar 4: la Furia es legible, nunca una emboscada — ver Visual/Audio KV-9). **Importante — límite de la garantía de ≥30s**: la garantía de AC-H29 (`telegraph_duration_s + raw_TTK ≥ 30s`) aplica **solo antes de la Furia**; en `ENRAGED`, por diseño, el telegrafío se acorta y el daño sube, así que la ventana de decisión se comprime deliberadamente — es el castigo por estancar la pelea o no comprometer héroes, no un incumplimiento del contrato con Héroes. `time_to_death`/`incoming_damage_rate_total` (Reglas 6/6b) se proyectan con los valores efectivos vigentes (enraged si aplica), de modo que el readout sigue siendo honesto también en Furia. + +11. **Congelamiento durante `DEATH_HOLD` — consumo de `game_time_paused` (contrato con Permadeath, cierra OQ-P1, 2026-08-16)**: mientras Permadeath mantiene un beat `DEATH_HOLD` en `HOLDING` (flag `game_time_paused=true`), el ciclo de intención completo del kaiju se congela — ningún reloj interno avanza: la evaluación de `threat_score` (Regla 3/F1) no recalcula, la cuenta regresiva de `telegraph_duration_s` no avanza, `STAGGERED` no cuenta `stagger_duration_s`, el contador de neutralización (Regla 7) no acumula ni se resetea, **la ventana deslizante de desenganche `hero_disengage_window_s` (F5, disparador (b) de Furia) no avanza**, y `STRIKING` no invoca `apply_damage` — ningún golpe del kaiju (telegrafiado o de esbirro) puede aterrizar mientras el beat esté activo. **Por qué la ventana de desenganche también se congela (cierra S7 del cross-review 2026-08-18):** durante `DEATH_HOLD` el input del jugador está suprimido (no puede dar órdenes ni enganchar héroes); si la ventana deslizante siguiera corriendo, un beat multi-muerte comprimido (Permadeath Regla 8) podría caminar el roster hacia un disparo **involuntario** de Furia con cero agencia — lo opuesto a lo que el disparador (b), que castiga el desenganche *consciente*, debe medir. Congelarla preserva que el conteo de compromiso solo corra cuando el jugador puede actuar. Este sistema **lee el flag cooperativamente** (polling en su propio `_process`), nunca depende de `SceneTree.paused` — mismo mecanismo que Permadeath exige a Combate/Daño (Regla 4 de `design/gdd/permadeath.md`, AC-C52). Al volver `game_time_paused=false`, el ciclo reanuda exactamente donde quedó — ningún reloj se resetea ni se adelanta para "compensar" el tiempo congelado. Esto es lo que sostiene la garantía del cap de bloqueo total de Permadeath (Regla 8 de ese GDD): si el kaiju no respetara este flag, podría aterrizar un golpe letal a mitad de un beat y romper el invariante de "`N` fijo por cadena" que esa regla depende. + +### States and Transitions + +Este sistema **no** posee el FSM de enganche de combate (`NO_TARGET`/`TARGET_ACQUIRED`/`ATTACKING`) — eso es de Combate/Daño y se aplica igual al kaiju y a los esbirros. Lo que sí posee es el **ciclo de intención del kaiju**, una capa exclusiva que decide cuándo y contra quién el kaiju entra al ciclo de Combate/Daño: + +| Estado | Descripción | Transición desde | Transición hacia | +|---|---|---|---| +| `SEEKING` | Sin objetivo telegrafiado; calculando la puntuación de amenaza por candidato (Regla 3) | Inicio de `CLIMAX`; fin de `STRIKING`; fin de `STAGGERED`; **cancelación de `TELEGRAPHING`** (el objetivo murió o salió de `aggro_range` sin completar el telegrafío — Edge Cases, AC-K06(c)/AC-K46) | `TELEGRAPHING` | +| `TELEGRAPHING` | Objetivo elegido y marcado (Regla 5); telegrafío visual/sonoro en curso (`telegraph_duration_s`); interrumpible por daño acumulado (Regla 7) | `SEEKING` | `STRIKING` (telegrafío completo sin interrupción); `STAGGERED` (neutralizado, Regla 7); **`SEEKING` (cancelado: objetivo murió/salió de `aggro_range` — no es neutralización, no acredita `P=1`, Edge Cases)** | +| `STRIKING` | El telegrafío se resuelve **invocando `apply_damage` directamente en ese frame**, saltándose el piso de adquisición/espera de cooldown del FSM base para esa única llamada (Regla 4, override explícito); el `attack_cooldown_s` del kaiju empieza a contar tras este golpe | `TELEGRAPHING` | `SEEKING` (tras resolver el golpe) | +| `STAGGERED` | Breve ventana de vulnerabilidad tras una neutralización exitosa (`stagger_duration_s`); el kaiju no ataca ni telegrafía durante este estado | `TELEGRAPHING` (vía neutralización) | `SEEKING` | + +- Los esbirros **no** participan de este ciclo — operan enteramente dentro del FSM de Combate/Daño sin capa adicional (Regla 8). +- **Todos los relojes de esta tabla** (`telegraph_duration_s`, `stagger_duration_s`, la ventana deslizante de amenaza F1, el contador de neutralización de Regla 7) **y la ventana deslizante de desenganche `hero_disengage_window_s` (F5, disparador (b))** dejan de avanzar mientras `game_time_paused=true` (Regla 11) — el estado actual se sostiene sin transicionar hasta que el flag vuelva a `false`. +- La entrada a `TELEGRAPHING` es el único punto donde este sistema escribe estado hacia Combate/Daño (marcado, Regla 5) y hacia Héroes (`time_to_death`/`incoming_damage_rate_total`, Reglas 6/6b) — el resto del ciclo es interno. +- **`ENRAGED` no es un estado del ciclo de intención, es un modificador ortogonal** (Regla 10): el kaiju sigue recorriendo `SEEKING`/`TELEGRAPHING`/`STRIKING`/`STAGGERED` normalmente cuando está enraged; la Furia solo altera los valores efectivos (`telegraph_duration_s`, `attack_damage`) vía F5. No hay transiciones nuevas — un flag latcheado. +- **Contrato de orden de tick entre los dos FSM (ahora con requisito de dueño único, no solo recomendación — 3er re-review)**: el kaiju participa simultáneamente del FSM de enganche de Combate/Daño (`NO_TARGET`/`TARGET_ACQUIRED`/`ATTACKING`, ajeno) y del ciclo de intención (propio). Durante `SEEKING`/`TELEGRAPHING` el kaiju permanece en `TARGET_ACQUIRED` del FSM base con su ataque **suprimido** (gate, Regla 4); el handoff a la resolución de golpe ocurre al entrar a `STRIKING`. El **outcome** de las carreras same-frame lo fija este GDD (prioridad absoluta de victoria/derrota, Edge Cases/AC-K50/K52), y **ya fija el mecanismo del golpe** (`STRIKING` invoca `apply_damage` directamente, síncrono, mismo frame, Regla 4). + + **Requisito duro (no diferible al ADR) — el kaiju tiene un único dueño de `apply_damage`, para cerrar el doble-golpe en el frame de `STRIKING`** (ai-programmer): en el frame exacto de entrada a `STRIKING`, el `attack_cooldown_s` del kaiju está en 0 y el objetivo está en rango — que es **exactamente** la condición con la que el bucle de auto-ataque del FSM base dispararía su propio `apply_damage` (Combate/Daño Regla 3). Como el gate de Regla 4 solo suprime durante `SEEKING`/`TELEGRAPHING` y **no** durante el frame de `STRIKING`, si el FSM base y el controlador de intención corrieran su lógica por separado en ese frame se producirían **dos** `apply_damage` en un frame (uno del override de `STRIKING`, otro del auto-ataque base). Para eliminar la carrera **de raíz** (no solo mitigarla con disciplina de poll), el kaiju **debe excluirse por completo del bucle genérico de auto-ataque de Combate/Daño**: el controlador de intención es el **único** llamador de `apply_damage` para el kaiju, en **todos** los estados. El `can_attack()` deja de ser un poll que el FSM base consulta cada frame y pasa a ser irrelevante para el kaiju porque el FSM base **nunca** evalúa auto-ataque sobre él. Esto es más limpio y Godot-idiomático que depender del orden de `_physics_process` entre nodos hermanos (que en 4.6 es determinista por orden de árbol/`process_priority`, pero depender de él implícitamente sigue siendo frágil). + + **Requisito duro — abort tras cruce de 0 en `STRIKING`** (ai-programmer, cierra la race de AC-K50): el handler de `STRIKING`, tras llamar `apply_damage`, **debe** comprobar si el kaiju cruzó 0 de vida **antes** de avanzar a `SEEKING` o re-seleccionar objetivo; si cruzó 0, aborta de inmediato (la victoria de Regla 9 tiene prioridad absoluta). No se apoya en el orden de fan-out de la señal de muerte: la comprobación es síncrona en el mismo handler, no un listener separado. + + **Aún para el ADR (`/architecture-decision`)**: el nodo/propietario concreto que aloja el controlador de intención y cómo se conecta la señal de `apply_damage` de Combate/Daño (debe ser modo inmediato, **no** `CONNECT_DEFERRED`, para que "victoria en ese instante" de Regla 9 sea síncrona). El ADR ejecuta estos requisitos; no los reabre. + +### Interactions with Other Systems + +| Sistema | Dirección | Interfaz | +|---|---|---| +| Combate/Daño | bidireccional | Este sistema usa el FSM de enganche y `apply_damage` de Combate/Daño sin modificarlos; sobreescribe la selección de objetivo (Regla 3) y el `leash_range` (Regla 2) **solo para el kaiju**; expone `marked_targets`/`marked_threats` que Combate/Daño lee para computar `P` (AC-C32/AC-C33, hoy mockeadas); consume `CombatProfile`, F2/F3 y `apply_damage` sin poseerlos. ✅ Diseñado (Approved) | +| Temporizador de Preparación/Ritual | Temporizador → este sistema | Consume la señal `entered_climax` (dispara Regla 1) y `kaiju_timer_remaining_ratio` fijado en 0 durante `CLIMAX`; este sistema **nunca** escribe de vuelta hacia Temporizador (contrato ya confirmado por Temporizador AC-T03b). ✅ Diseñado (Approved) | +| Datos de Era/Civilización | Datos → este sistema | Lee `kaiju_definition` (stats del kaiju) y, si la era los define, los datos de esbirros (roster/oleadas, `max_concurrent_esbirros`) de la era `ACTIVE`. ⚠️ In Design — la interfaz `kaiju_definition` que este sistema necesita ya está estable en ese GDD aunque el documento completo no esté Approved | +| Sistema de Héroes | este sistema → Héroes | Expone por héroe `time_to_death` (kaiju-only, Regla 6) **e** `incoming_damage_rate_total` (todas las fuentes, Regla 6b) — **resuelve** el contrato bloqueado de Héroes AC-H29/U-3 (ventana de visibilidad de ~30s) y cierra el hueco de honestidad de daño de esbirros (Pilar 4). El readout de Héroes debe reflejar la fuente más peligrosa de las dos | ✅ Diseñado (Approved) — desbloquea sus ACs mockeadas | +| Transición de Era | este sistema → Transición | Notifica el veredicto de victoria/derrota de la era (Regla 9); Transición de Era **posee** qué ocurre después (props de legado, avance narrativo) — este sistema solo declara el resultado, no gestiona la consecuencia. *(Provisional — sin GDD)* | +| UI/HUD | este sistema → UI | Expone el estado del ciclo de intención (`SEEKING`/`TELEGRAPHING`/`STRIKING`/`STAGGERED`), el flag `ENRAGED`, el objetivo marcado, `time_to_death` e `incoming_damage_rate_total` proyectados para que UI/HUD renderice el telegrafío legible (Player Fantasy: nunca emboscar). *(Provisional — sin GDD)* | +| Audio | este sistema → Audio | Las transiciones del ciclo de intención (especialmente la entrada a `TELEGRAPHING`) son la fuente de verdad para cues de advertencia del kaiju. *(Provisional — sin GDD)* | +| TimeControl (servicio Core) | TimeControl → este sistema | Este sistema lee `game_time_paused` de **TimeControl** (ADR-0002, Accepted 2026-08-21) cooperativamente (polling, nunca `SceneTree.paused`) y congela su ciclo de intención completo mientras el régimen efectivo sea `PAUSED` (Regla 11). La fuente del régimen es Permadeath (solicita `PAUSED` en `DEATH_HOLD`); el dueño del flag es TimeControl. Sus relojes de tiempo de juego (`climax_elapsed_s`, ventana F1, etc.) derivan de `TimeControl.game_tick`. | + +## Formulas + +Este sistema tiene seis fórmulas propias (F1–F6) — todas propuestas y verificadas contra casos límite por `systems-designer`. Ninguna reabre ni redefine las fórmulas de Combate/Daño (F1-F5) o Temporizador (F1-F4); las consumen. + +**Determinismo de las fórmulas (contrato de testeo)**: toda aleatoriedad de este sistema (`random_variation` de F1, Regla 3) usa una instancia de `RandomNumberGenerator` **inyectable/seedeable** por el arnés de tests — nunca `randf`/`randi` globales sin seed. Esto permite que los AC exactos (AC-K14–K17, y en particular el desempate exacto AC-K48) sean deterministas y repetibles, coherente con la regla "Determinism: tests must produce the same result every run — no random seeds" de coding-standards. Todos los relojes de tiempo de este sistema (`climax_elapsed_s`, ventanas de F1/Regla 7/Regla 10) acumulan **tiempo de juego por delta en `_physics_process`** (mismo patrón framerate-independiente que Temporizador AC-T21), nunca tiempo real ni conteo de frames — el pacing no depende del framerate. + +**`entity_id` — identificador estable, autoral, NO el `get_instance_id()` de Godot (definición, corrección del 3er re-review)**: el desempate determinista de `threat_score` (AC-K48) y de cualquier otra selección de este sistema usa un `entity_id` que es un **`int` estable asignado en el spawn por un contador monótono de encuentro** (0, 1, 2, … en orden de instanciación dentro del `CLIMAX`), poseído por este sistema o por el spawner de combatientes — **nunca** el `get_instance_id()` de Godot, cuyo valor depende del orden/reciclaje de asignación de instancias del engine y **no es reproducible entre corridas** (rompería el desempate determinista que AC-K48/AC-K71 exigen). El contador se reinicia a 0 en `entered_climax` y es determinista bajo el mismo orden de spawn (que a su vez es determinista: oleadas por F3 + roster fijo de la era). *(Nota: Combate/Daño **no** define un campo de desempate propio — su AC-C06 resuelve "más cercano" sin cláusula de empate; este `entity_id` es una definición nueva de este sistema, no una herencia de aquél — ver AC-K48.)* + +**Determinismo cross-plataforma de la RNG (limitación conocida, godot-specialist)**: `RandomNumberGenerator` (PCG) es determinista para un mismo `seed` + secuencia de llamadas **en el mismo build/arquitectura** (editor y export en la misma máquina inclusive). No está garantizado que la salida de punto flotante de `randf_range`/`randi_range` sea **bit-idéntica entre arquitecturas de export distintas** (p. ej. CI en x64 vs. un export de consola). Los AC de desempate exacto (AC-K48/AC-K71) se testean contra la **plataforma de export objetivo**, o se acotan a determinismo same-plataforma — no se asume identidad byte-a-byte cross-arquitectura (ver AC-K71). + +### F1 — Puntuación de amenaza (`threat_score`, Regla 3) + +`threat_score = (threat_damage_weight × recent_damage_dealt) + (hero_threat_bonus × is_hero) + random_variation` + +| Variable | Símbolo | Tipo | Rango | Descripción | +|---|---|---|---|---| +| Daño reciente al kaiju | `recent_damage_dealt` | float | ≥0 | Suma de daño infligido al kaiju por este candidato, acumulado en una ventana deslizante de `threat_window_s` segundos (acumulado, no tasa — sin división) | +| Ventana de amenaza | `threat_window_s` | float (knob) | 3.0–12.0 (MVP: **6.0**) | Duración de la ventana deslizante; `≤0` → loud-fail al cargar | +| Peso del daño | `threat_damage_weight` | float (knob) | 0.5–2.0 (MVP: **1.0**) | Escala cuánto pesa el daño reciente frente al bono de héroe | +| ¿Es héroe? | `is_hero` | bool | {0,1} | 1 si el candidato es un héroe, 0 si es tropa | +| Bono de héroe | `hero_threat_bonus` | float (knob) | 50–200 (MVP: **100**) | Peso fijo que favorece héroes sobre tropas | +| Variación aleatoria | `random_variation` | float | `random_uniform(-threat_random_range, +threat_random_range)` | Rompe el patrón determinista explotable | +| Rango de aleatoriedad | `threat_random_range` | float (knob) | 5–30 (MVP: **15**) | Amplitud de la variación aleatoria | +| **Puntuación de amenaza** | `threat_score` | float | `[-threat_random_range, ∞)` | El candidato con mayor `threat_score` es seleccionado | + +**Output range**: sin cota superior (el daño acumulado crece sin límite); acotado por abajo en `-threat_random_range`. Sin división en la fórmula — estructuralmente no hay divide-by-zero. + +**Algoritmo de `recent_damage_dealt` (ventana deslizante, contrato de implementación)**: por candidato, se mantiene una lista de eventos de daño `(timestamp_climax_s, amount)` contra el kaiju; `recent_damage_dealt` en el instante `t` es la suma de `amount` de los eventos con `timestamp ≥ t − threat_window_s`. Los eventos más viejos que la ventana se descartan (poda perezosa al consultar o en cada tick). Alternativa equivalente permitida: suma con decaimiento por bucket temporal — pero **no** un promedio exponencial continuo, porque cambiaría la semántica de "ventana fija" que los AC asumen. La acumulación de eventos de daño ocurre **de forma continua durante todos los estados del ciclo de intención** (`SEEKING`/`TELEGRAPHING`/`STRIKING`/`STAGGERED`), no solo en `SEEKING` — lo que cambia solo en `SEEKING` es cuándo se **evalúa** `threat_score` para seleccionar objetivo (Regla 3, AC-K09), no cuándo se registra el daño. **Nota — no confundir con el contador de neutralización (Regla 7)**: son dos acumuladores de daño distintos con semánticas distintas — este es una ventana deslizante por-candidato de `threat_window_s` (para selección de objetivo); el de Regla 7 es un contador simple que se **resetea a 0 en cada `SEEKING→TELEGRAPHING`** (para interrumpir un telegrafío). Nunca comparten estado. + +**Ejemplo**: `threat_window_s=6.0`. Vanguardia (héroe) infligió 40 de daño reciente; un melee_infantry infligió 24; un support_troop infligió 0. `threat_score(Vanguardia) = 1.0×40 + 100×1 + r ∈ [125,155]`; `threat_score(melee) = 1.0×24 + 0 + r ∈ [9,39]`; `threat_score(support) = 0 + 0 + r ∈ [-15,15]`. Vanguardia gana de forma determinista (su mínimo supera el máximo de cualquier otro candidato). + +**Caso límite — sin historial de daño (inicio de `CLIMAX`)**: con un héroe presente y ningún candidato habiendo dañado aún al kaiju, el héroe sigue ganando de forma determinista por el bono fijo (`[85,115]` vs. `[-15,15]` de cualquier tropa) — comportamiento intencional, el kaiju prefiere objetivos de alto valor incluso sin historial. Si **solo hay tropas** en rango, todas caen en el mismo rango y la selección la resuelve puramente el término aleatorio — azar acotado entre candidatos válidos, deliberado: la Regla 3 existe para *anular* el aggro por cercanía de Combate/Daño (Regla 4) solo para el kaiju, así que un fallback silencioso por distancia reintroduciría exactamente lo que la regla anula. + +**Decisión de diseño — el screening de tropas NO es una capa táctica del MVP (adjudicado 2026-08-11)**: con los defaults MVP, el sesgo héroe>tropa es tan fuerte (`hero_threat_bonus=100` fijo vs. daño de tropa) que "escudar al héroe interponiendo tropas para desviar el aggro del kaiju" **no** es una jugada viable — el kaiju re-adquiere al héroe casi cada ciclo. Esto es **intencional**: encarna "el kaiju viene por ti" del Player Fantasy. El posicionamiento del jugador es **comprometer o retirar** (mantener al héroe en `aggro_range` y arriesgarlo, o sacarlo de `aggro_range`, AC-K06(b)), no un espectro de escudado con tropas. La retirada binaria es la palanca de decisión deseada para el MVP; el "ajedrez táctico" del Player Fantasy se juega en *a quién comprometes contra qué telegrafío* y en *neutralizar vs. proteger* (Regla 5/7), no en desviar aggro. Revisar tras playtest (OQ-8): si el combate se siente demasiado determinista, el screening de tropas es la primera capa candidata a añadir. + +**Cuantificación del punto de inversión del sesgo (para re-tuneo)**: una tropa con daño reciente sostenido *puede* superar a un héroe recién llegado (sin historial) cuando `threat_damage_weight × recent_damage_dealt > hero_threat_bonus + 2 × threat_random_range`, es decir `recent_damage_dealt > (hero_threat_bonus + 2 × threat_random_range) / threat_damage_weight`. Con MVP (`1.0`, `100`, `15`): `> 130` de daño acumulado en la ventana de `6.0s` (≈21.7 DPS sostenidos), improbable para una sola tropa. Pero en los **extremos del rango seguro** (`threat_damage_weight=2.0`, `hero_threat_bonus=50`, `threat_random_range=30`): el umbral cae a `> 55` de daño en hasta `12.0s` de ventana (≈4.6 DPS) — una tropa que golpee sostenidamente **invierte** el sesgo héroe>tropa. Por eso los tres knobs de F1 están acoplados (ver Tuning Knobs): re-tunear uno sin los otros dos puede romper la propiedad "el kaiju viene por ti" sin que ninguna validación formal lo detecte. + +### F2 — Proyección `time_to_death` (Regla 6, resuelve Combate/Daño OQ-5) + +`time_to_death = telegraph_duration_s + (kill_time_hits_kaiju × attack_cooldown_s_kaiju)` + +donde `kill_time_hits_kaiju = ceil(hero_HP_at_telegraph_start / kaiju_attack_damage)` (Combate/Daño F2) y `attack_cooldown_s_kaiju` proviene del `CombatProfile` del kaiju. **En la era MVP** estos dos valores son `kaiju_attack_damage=45` y `attack_cooldown_s=4.5`. + +> ⚠️ **Estos dos valores NO están marcados como "locked" en Combate/Daño** — ese GDD los trata como knobs tunables por tipo (`attack_damage ≥ 1`, `attack_cooldown_s ≥ 0.1`, AC-C03: "cambiar el resource cambia el comportamiento sin tocar código"). La garantía de ≥30s de Héroes (AC-H29) **depende directamente** de ellos: si un futuro re-tuning en Combate/Daño los cambia, F2 de este GDD y el mínimo de `telegraph_duration_s` (abajo) deben re-verificarse. Esta es una **restricción cross-GDD explícita** (ver Tuning Knobs y OQ-10) — no una asunción de que estén congelados. La corrección de largo plazo (formalizar el lock o un trigger de re-verificación en Combate/Daño) es OQ-10. + +| Variable | Símbolo | Tipo | Rango | Descripción | +|---|---|---|---|---| +| Duración del telegrafío | `telegraph_duration_s` | float (knob) | [12.5, 17.0] (MVP: **14.0**) | Tiempo entre `SEEKING→TELEGRAPHING` y la resolución del golpe | +| Golpes para matar (kaiju) | `kill_time_hits_kaiju` | int | ≥1 | Combate/Daño F2 | +| Vida del héroe al iniciar telegrafío | `hero_HP_at_telegraph_start` | int | [1, hero_max_health] | **Snapshoteada** al entrar a `TELEGRAPHING` — no se re-lee después (mismo patrón que Héroes snapshotea D/T/P) | +| **Tiempo hasta la muerte** | `time_to_death` | float | ≥ `telegraph_duration_s` + 4.5 | Consumido por Héroes AC-H29/U-3 | + +**Derivación del mínimo de `telegraph_duration_s`**: AC-H29/U-3 exige ≥~30s, y el caso vinculante es el héroe de **menor** TTK crudo (no el promedio) — Vanguardia/Portaestandarte, TTK crudo 18.0s (Combate/Daño): `telegraph_duration_s + 18.0 ≥ 30.0` → `telegraph_duration_s ≥ 12.0s` exacto, cero margen. El Guardián (TTK crudo 22.5s) queda cubierto con margen de sobra. **MVP: 14.0s** — 2s de margen sobre el mínimo matemático, para no aterrizar exactamente en el borde de un AC bloqueante; el techo del rango seguro (17.0s) mantiene la ventana del Guardián bajo ~40s para no romper el "breve e intenso" del Pilar 3. + +**Ejemplos** (`telegraph_duration_s=14.0`, héroe a HP completo): Vanguardia (140 HP) → `kill_time_hits=4` → `time_to_death=14.0+18.0=32.0s` ✓. Guardián (200 HP) → `kill_time_hits=5` → `time_to_death=14.0+22.5=36.5s` ✓. + +**Límite de la garantía (declarado explícitamente, no un piso universal)**: la garantía de ≥30s se sostiene para el **primer telegrafío contra un héroe a HP completo o casi completo**, **antes de la Furia** (Regla 10 — en `ENRAGED` el telegrafío se acorta a propósito y la garantía no aplica). Un héroe ya dañado (por golpes previos del kaiju o de esbirros) puede tener `kill_time_hits_kaiju` mucho menor — p. ej. a HP crítico, `kill_time_hits=1` da `time_to_death=14.0+4.5=18.5s < 30s`. No es un defecto de la fórmula: no se le puede prometer 30s a un héroe casi muerto. Además, esta proyección modela **solo** el daño del kaiju — daño simultáneo de esbirros contra el mismo héroe no se descuenta en `time_to_death`; la señal honesta de muerte multi-fuente es `incoming_damage_rate_total` (Regla 6b / F6), que el readout de Héroes debe reflejar (ver Edge Cases). + +**Restricción de autoría de era (robustez de la garantía frente al rango de HP de Héroes)**: `sistema-de-heroes.md` documenta `hero_max_health ∈ [120, 200]` (multiplicador `[3.0, 5.0]`). La derivación de `telegraph_duration_s ≥ 12.0s` de abajo solo verifica los tres arquetipos ilustrativos (140/150/200 HP). Un héroe de **120 HP** (multiplicador 3.0, dentro de rango) da `kill_time_hits = ceil(120/45) = 3` → raw TTK `= 3 × 4.5 = 13.5s` → con `telegraph_duration_s=14.0`, `time_to_death = 27.5s < 30s` — **rompería AC-H29**. Por tanto, para que la garantía de ≥30s se sostenga contra el kaiju MVP (`45`/`4.5`), el roster de héroes de una era que lo enfrente **debe** cumplir `raw_TTK(héroe) ≥ 30 − telegraph_duration_s` (con MVP `telegraph_duration_s=14.0` → `raw_TTK ≥ 16.0s` → `ceil(HP/45) ≥ 4` → **`hero_max_health ≥ 136`**). Los tres arquetipos MVP (140/150/200) lo cumplen holgadamente; un héroe de 120–135 HP no. Esta es una **restricción de autoría por era, ahora un check obligatorio de loud-fail al cargar `EraDefinition`** (no una guía manual): para cada héroe de la era, `ceil(hero_max_health / kaiju_attack_damage) × kaiju_attack_cooldown_s + telegraph_duration_s ≥ 30`; si algún héroe lo viola, la carga de la era falla ruidosamente. El check es computable con datos ya disponibles al cargar la era, así que se enforce, no se documenta y confía en el autor (ver Edge Cases "Config inválido cross-GDD", AC-K74; resuelve OQ-11). + +### F3 — Cadencia de oleadas de esbirros (Regla 8, si la era los define) + +`wave_count(t) = 0` si `t < first_wave_delay_s`; si no, `clamp(floor((t − first_wave_delay_s) / wave_interval_s) + 1, 0, max_waves)` + +donde `t = climax_elapsed_s` (reloj propio de este sistema, reinicia a 0 en `entered_climax` — independiente del reloj de Temporizador). + +| Variable | Símbolo | Tipo | Rango | Descripción | +|---|---|---|---|---| +| Tiempo transcurrido en clímax | `climax_elapsed_s` | float | ≥0 | Reinicia a 0 en `entered_climax` | +| Retraso de la primera oleada | `first_wave_delay_s` | float (knob) | 5–20 (MVP: **10.0**) | Antes de este tiempo, cero esbirros | +| Intervalo entre oleadas | `wave_interval_s` | float (knob) | 15–40 (MVP: **20.0**) | `≤0` → loud-fail al cargar (evita división por 0 en el floor) | +| Tope de oleadas | `max_waves` | int (dato de era) | ≥0 | 0 = era sin esbirros (Regla 1/8) | +| **Cuenta de oleadas objetivo** | `wave_count` | int | [0, max_waves] | Monótono no-decreciente en `t`, nunca negativo | +| Tamaño de oleada | `esbirro_wave_size` | int (dato de era) | ≥1 | Unidades instanciadas cuando `wave_count` sube en 1 | + +**Output range**: `[0, max_waves]`, boundary-safe por construcción — `max_waves=0` hace que una era sin esbirros no necesite una rama de código separada (Regla 8, "si la era los define"), solo un dato en 0. + +**Ejemplo** (`first_wave_delay_s=10.0`, `wave_interval_s=20.0`, `max_waves=4`, `esbirro_wave_size=3`): `t=5s→wave_count=0`; `t=10s→1` (oleada 1, +3); `t=25s→1` (sin nueva oleada aún); `t=30s→2` (oleada 2, +3); `t=200s→10→clamp a 4` (tope alcanzado). + +**Nota de implementación (no es un caso límite de la fórmula, es un contrato de uso)**: `wave_count(t)` es el **conteo acumulado objetivo**, no un evento de spawn. El sistema de spawn debe llevar `last_spawned_wave_count` e instanciar solo `(wave_count(t) − last_spawned_wave_count) × esbirro_wave_size` unidades cuando `wave_count` sube — evaluar la fórmula cada tick y spawnear `wave_count × esbirro_wave_size` unidades en cada evaluación re-spawnearía oleadas ya instanciadas. `climax_elapsed_s` acumula tiempo de juego por delta en `_physics_process` (ver nota de determinismo al inicio de Formulas) — `wave_count(t)` es framerate-independiente por construcción. + +**Gate de concurrencia (`max_concurrent_esbirros`, Regla 8b)**: el spawn de la(s) oleada(s) debida(s) se **difiere** mientras el conteo de esbirros vivos alcance `max_concurrent_esbirros`. Formalmente, en cada tick el spawner instancia `spawnable = min((wave_count(t) − last_spawned_wave_count) × esbirro_wave_size, max(0, max_concurrent_esbirros − alive_esbirro_count))` unidades, e incrementa `last_spawned_wave_count` solo por las oleadas **completas** efectivamente instanciadas (una oleada parcialmente instanciada por el cap mantiene su remanente pendiente para el próximo tick que tenga espacio). `wave_count(t)` (cadencia-objetivo) nunca se ve afectado — solo el momento real de instanciación. Esto acota el peor caso de esbirros en pantalla a `max_concurrent_esbirros` sin importar `max_waves × esbirro_wave_size`. + +### F4 — `threat_neutralize_damage_threshold` (Regla 7) + +`threat_neutralize_damage_threshold = kaiju_attack_damage × neutralize_hits_equivalent` + +| Variable | Símbolo | Tipo | Rango | Descripción | +|---|---|---|---|---| +| Daño de ataque del kaiju | `kaiju_attack_damage` | int | 45 (locked, CombatProfile de Combate/Daño) | Ancla del umbral — anclado a un valor ya definido, sin depender de `kaiju_max_health` (que ningún GDD define aún) | +| Golpes-equivalentes para neutralizar | `neutralize_hits_equivalent` | int (knob) | 2–5 (MVP: **3**) | Cuántos "golpes de kaiju" de daño acumulado hacen falta para cancelar su propio ataque telegrafiado | +| **Umbral de neutralización** | `threat_neutralize_damage_threshold` | int | 90–225 (MVP: **135**) | Daño acumulado desde el inicio del `TELEGRAPHING` actual (Regla 7) que cancela el ataque | + +**Output range**: `(0, ∞)` en la fórmula misma; el rango seguro del knob (2-5) lo acota a `[90,225]`. `neutralize_hits_equivalent=0` haría el umbral 0 — cualquier golpe, incluso accidental, cancelaría el ataque, rompiendo la Regla 7 (la interrupción debe requerir esfuerzo real); loud-fail si `≤0`. + +**⚠️ Constraint de acoplamiento `neutralize_hits_equivalent × telegraph_duration_s` — ahora ENFORCED como loud-fail (3er re-review)**: la "propiedad de una sola tropa no neutraliza en solitario" NO se sostiene en todo el rango seguro de knobs individualmente válidos, así que — como la garantía ≥30s (F2/AC-K74) — deja de ser prosa de disciplina de autor y pasa a ser un **check de datos obligatorio al cargar `EraDefinition`**. Ejemplos de la ruptura: con `neutralize_hits_equivalent=2` (`threshold=90`), una tropa a 8 DPS neutraliza en `90/8 = 11.25s`, por debajo de *cualquier* `telegraph_duration_s` válido (piso 12.5s); e incluso con el MVP `threshold=135`, si `telegraph_duration_s=17.0` (techo), `135/8 = 16.875s < 17.0s` — una tropa sola sí neutraliza. + +**Check enforced (loud-fail al cargar la era, AC-K85)**: para toda era donde "ninguna tropa sola neutraliza" sea propiedad activa, debe cumplirse + +`kaiju_attack_damage × neutralize_hits_equivalent > telegraph_duration_s × max_single_troop_dps` + +donde `max_single_troop_dps` = el DPS de la tropa **más letal** del `unit_roster` de la era (no la menos letal — corrección del 3er re-review: la prosa previa usaba `min_troop_dps`, el bound equivocado; para garantizar que *ninguna* tropa neutralice hay que acotar contra la **más** fuerte, no la más débil). Todos los operandos son computables al cargar la era (`kaiju_attack_damage` del `kaiju_definition`, DPS de cada tropa del `unit_roster` — mismo patrón cross-resource que AC-K74). Si una era **quiere** deliberadamente permitir que una tropa élite neutralice, marca el flag opcional `allow_single_troop_neutralize=true` en su `kaiju_definition` para saltar el check conscientemente (default `false`); sin el flag, violar la desigualdad es loud-fail, no clamp. + +**Sanity-check de DPS** (con MVP `threat_neutralize_damage_threshold=135`, `telegraph_duration_s=14.0` como ventana disponible): escuadrón de 2 `melee_infantry` (8 DPS c/u) + Portaestandarte (12.7 DPS) = 28.7 DPS combinado → `135/28.7≈4.7s`, cómodo dentro de la ventana — exige compromiso real pero alcanzable. 1 `melee_infantry` solo (8 DPS) → `135/8≈16.9s > 14.0s` de ventana — **no puede neutralizar en solitario**, necesita apoyo. Vanguardia sola (27.8 DPS) → `135/27.8≈4.9s` — **un héroe solo sí puede neutralizar**, propiedad de diseño deseable (un héroe comprometido vale por varias tropas). + +*(Nota para Open Questions: si una futura era introduce un kaiju con `attack_damage` muy distinto a 45, este umbral se re-escala automáticamente vía la fórmula — pero el knob `neutralize_hits_equivalent` en sí es un valor de feel que debería revisarse por kaiju, no asumirse universal. Migrar a una fracción de `kaiju_max_health` es la alternativa de largo plazo, bloqueada hasta que `kaiju_definition` defina ese campo.)* + +### F5 — Furia del kaiju (`ENRAGED`, Regla 10) + +**Disparador (latcheado — una vez `true`, permanece `true`):** + +`enraged = (climax_elapsed_s ≥ enrage_time_threshold_s) OR (engaged_hero_count(t) < required_engaged_heroes(t))` + +**Disparador (a) — por tiempo**: `climax_elapsed_s ≥ enrage_time_threshold_s` (cota dura del *inicio de la escalada*, no de la duración total — ver Nota de robustez). + +**Disparador (b) — por desenganche consciente del roster** (Regla 10(b)): +- `engaged_hero_count(t)` = número de héroes **vivos comprometidos** dentro de la ventana deslizante `[t − hero_disengage_window_s, t]`. Un héroe está **comprometido** si infligió daño al kaiju **o a cualquier esbirro** dentro de la ventana (exención de esbirros, Regla 10b — limpiar una oleada cuenta como estar en la pelea). Mismo patrón de ventana deslizante que F1; por héroe se guarda su `last_combat_damage_climax_s` (timestamp del último daño infligido a kaiju **o** esbirro) y se cuenta si `t − last_combat_damage_climax_s ≤ hero_disengage_window_s`. +- `required_engaged_heroes(t) = clamp(floor(hero_engagement_fraction × living_hero_count(t)) + 1, 1, living_hero_count(t))` mientras `living_hero_count(t) ≥ 1`; si `living_hero_count(t) = 0` la condición (b) es vacuamente falsa (la derrota, Regla 9, ya se disparó al morir el último héroe, así que este caso no se evalúa en la práctica). **Semántica `floor(f×N)+1` (corregida en el 3er re-review)**: requiere **estrictamente más de** `hero_engagement_fraction` del roster vivo, no "al menos" (que era `ceil`). Al default `f=0.5` esto es exactamente **> N/2** (más de la mitad) para todo `N`, cerrando la degeneración de `ceil(0.5×2)=1` que dejaba pasar un héroe-cebo único con exactamente 2 héroes vivos. El knob se preserva: bajarlo hacia `0.34` relaja el listón (en rosters pequeños vuelve a "algún héroe"), subirlo hacia `1.0` lo endurece a "todos". +- El disparador (b) se cumple en el instante `t` en que `engaged_hero_count(t) < required_engaged_heroes(t)`. Como es un chequeo instantáneo sobre una ventana deslizante (no un "tiempo desde"), es inherentemente consciente del roster: mantener comprometido a un solo héroe-cebo deja `engaged_hero_count=1`, que cae por debajo de `required_engaged_heroes` en cuanto el roster vivo es ≥2 (con `f=0.5`: `N=2 → floor(1.0)+1 = 2 > 1`; `N=3 → floor(1.5)+1 = 2 > 1`; `N=4 → floor(2.0)+1 = 3 > 1`). Con un solo héroe vivo, `required_engaged_heroes = clamp(floor(0.5×1)+1, 1, 1) = 1`, y la condición degenera correctamente a "ese único héroe debe estar comprometido dentro de la ventana". **Degradación con bajas** (f=0.5): `N=4→3`, `N=3→2`, `N=2→2`, `N=1→1` — el listón baja al morir héroes, nunca exige comprometer héroes que ya no existen. + +**Valores efectivos cuando `enraged`:** + +`telegraph_duration_s_effective = telegraph_duration_s × enrage_telegraph_factor` +`attack_damage_effective = round(kaiju_attack_damage × enrage_damage_factor)` + +| Variable | Símbolo | Tipo | Rango | Descripción | +|---|---|---|---|---| +| Umbral de tiempo de Furia | `enrage_time_threshold_s` | float (knob) | 90–240 (MVP: **150.0**) | Cota dura del **inicio de la escalada** (no de la duración total — ver Nota de robustez); `≤0` → loud-fail | +| Ventana de desenganche | `hero_disengage_window_s` | float (knob) | 15–30 (MVP: **15.0**) | Ventana deslizante para contar héroes comprometidos (disparador (b)); piso subido de 8 a **15** para que una retirada táctica de reposicionamiento normal (<15s) no dispare Furia — solo el desenganche sostenido; `≤0` → loud-fail | +| Fracción de compromiso de roster | `hero_engagement_fraction` | float (knob) | 0.34–1.0 (MVP: **0.5**) | Umbral de compromiso de roster para NO disparar Furia (disparador (b), consciente del roster): se exige **estrictamente más de** esta fracción de héroes vivos comprometidos (a kaiju o esbirros) dentro de la ventana, vía `required = floor(f×N)+1` (F5). Al default 0.5 = **> mitad** (cierra la degeneración N=2); 1.0 = todos los vivos (muy agresivo); 0.34 = en rosters pequeños vuelve a "algún héroe". **Dominio loud-fail estrechado a `[0.34, 1.0]`** (antes `(0,1]`): valores por debajo de 0.34 ya no pasan la validación en silencio — reabrían la degeneración de héroe-cebo en rosters grandes (p. ej. `f=0.1`, `N=9 → floor(0.9)+1 = 1`). `< 0.34` o `> 1.0` → loud-fail | +| Factor de telegrafío en Furia | `enrage_telegraph_factor` | float (knob) | 0.4–0.8 (MVP: **0.6**) | Multiplica `telegraph_duration_s` (<1 = más corto); `∉ (0,1]` → loud-fail | +| Factor de daño en Furia | `enrage_damage_factor` | float (knob) | 1.0–2.5 (MVP: **1.5**) | Multiplica `attack_damage` (≥1 = igual o más letal); `<1` → loud-fail | +| **¿Enraged?** | `enraged` | bool | {false,true} | Monótono: nunca vuelve a `false` dentro de un mismo `CLIMAX` | + +**Output**: `enraged` es monótono no-decreciente — una vez que **cualquier** disparador se cumple en algún frame, el flag latchea `true` para el resto del `CLIMAX` (aunque el disparador (b), que es un chequeo instantáneo sobre una ventana deslizante, volvería a ser falso si el jugador re-comprometiera héroes después: no importa, el latch ya ocurrió). `enrage_telegraph_factor < 1` empuja el telegrafío por debajo del piso de 12.0s de AC-H29 **a propósito** — la garantía de ≥30s aplica solo pre-Furia (Regla 10). `enrage_damage_factor` es la única excepción documentada a "daño plano" de Combate/Daño Regla 5. + +**Ejemplo disparador (a)** (MVP): a `climax_elapsed_s=150.0`, el kaiju enrage: `telegraph_duration_s_effective = 14.0 × 0.6 = 8.4s`; `attack_damage_effective = round(45 × 1.5) = 68`. Contra un Guardián a HP completo (200): `kill_time_hits = ceil(200/68) = 3` → `time_to_death = 8.4 + 3×4.5 = 21.9s` (vs. 36.5s pre-Furia) — la ventana de decisión se comprime deliberadamente. La proyección sigue siendo honesta (usa los valores efectivos). + +**Ejemplo disparador (b)** (MVP, `hero_engagement_fraction=0.5`, `hero_disengage_window_s=15`): con 4 héroes vivos, `required_engaged_heroes = floor(0.5×4)+1 = 3`. Si el jugador deja un solo héroe-cebo comprometido y retira a los otros 3, `engaged_hero_count = 1 < 3` → Furia se dispara. Para evitarla debe mantener ≥3 de sus 4 héroes comprometidos (a kaiju **o** esbirros) dentro de cada ventana de 15s (rotándolos si quiere protegerlos). Si 2 héroes mueren (2 vivos), `required_engaged_heroes = floor(0.5×2)+1 = 2` — con 2 vivos hacen falta los 2; el listón baja con las bajas pero un cebo único nunca basta con ≥2 vivos (cierra la degeneración N=2). Con 1 vivo, `floor(0.5×1)+1 = 1` — ese héroe debe estar comprometido. + +**Nota de robustez y límite de la cota de duración (decisión MVP consciente — aceptada 2026-08-12)**: los disparadores de Furia acotan *cuándo empieza* la escalada (disparador (a), 150s) y *fuerzan compromiso de roster* (disparador (b)), pero con `enrage_damage_factor`/`enrage_telegraph_factor` fijos la Furia **no** garantiza una cota dura de duración total: un jugador con DPS alto y neutralización perfecta podría *teóricamente* sobrevivir indefinidamente aun enraged. **Para el MVP esto se acepta a sabiendas como riesgo documentado, no como bug** — el disparador (a) es el techo del inicio de escalada, no un temporizador de muerte súbita. La palanca de cierre duro es un **ramp progresivo post-Furia** (`attack_damage_effective` sube un incremento por cada telegrafío resuelto tras la Furia, hasta one-shotear garantizado) — deliberadamente **fuera de alcance del MVP**, a validar en playtest (OQ-12). Si el playtest muestra estancamientos persistentes tras la Furia, OQ-12 es la primera palanca a implementar. *(La afirmación previa de que `enrage_time_threshold_s` era una "cota dura de duración" era imprecisa y se corrigió: es una cota del inicio de la escalada.)* + +### F6 — Tasa de daño entrante total (`incoming_damage_rate_total`, Regla 6b) + +`incoming_damage_rate_total(hero) = kaiju_projected_dps(hero) + Σ esbirro_dps(e)` para cada esbirro `e` actualmente enganchado a `hero` + +donde `kaiju_projected_dps(hero) = attack_damage_effective / attack_cooldown_s_kaiju` si el kaiju está telegrafiando/atacando a ese héroe, si no `0`; y `esbirro_dps(e) = e.attack_damage / e.attack_cooldown_s` (del `CombatProfile` del esbirro, Combate/Daño DPS). + +| Variable | Símbolo | Tipo | Rango | Descripción | +|---|---|---|---|---| +| DPS proyectado del kaiju sobre el héroe | `kaiju_projected_dps` | float | ≥0 | 0 si el kaiju no está enganchado a ese héroe | +| DPS de un esbirro enganchado | `esbirro_dps` | float | >0 | `attack_damage / attack_cooldown_s` del esbirro | +| **Tasa de daño entrante total** | `incoming_damage_rate_total` | float | ≥0 | Suma sobre todas las fuentes que golpean a ese héroe en el instante de lectura | + +**Uso**: el readout de Héroes (U-3) compara `time_to_death` (kaiju-only, F2) contra `hero_HP_current / incoming_damage_rate_total`; si el segundo es menor, la muerte real llega antes y el readout debe reflejar la fuente más peligrosa (cierra el hueco de honestidad del Pilar 4, Regla 6b). **Output range**: `≥0`; si `incoming_damage_rate_total=0` (nadie golpea al héroe), no se computa una división (no hay proyección de muerte por daño entrante — el héroe no está bajo ataque). Sin divide-by-zero: la división `hero_HP / rate` la hace el consumidor solo cuando `rate > 0`. + +## Edge Cases + +- **Config inválido de los nuevos knobs** (`threat_window_s ≤ 0`, `wave_interval_s ≤ 0`, `neutralize_hits_equivalent ≤ 0`, `telegraph_duration_s` fuera de `[12.5, 17.0]`, `enrage_time_threshold_s ≤ 0`, `hero_disengage_window_s ≤ 0`, `hero_engagement_fraction ∉ (0,1]`, `enrage_telegraph_factor ∉ (0,1]`, `enrage_damage_factor < 1`, `max_concurrent_esbirros < 0`, o cualquier negativo): **loud-fail al cargar** (no clamp) — mismo patrón que Combate/Daño y Temporizador. Cada caso es una fórmula rota (F1 degenera a ignorar daño reciente, F3 divide por cero en el floor, F4 vuelve la neutralización trivial o inalcanzable, F5 invierte el sentido de la Furia o hace `required_engaged_heroes` degenerado), no un extremo válido. +- **Config inválido cross-GDD — la garantía de ≥30s (AC-H29) se rompe con valores individualmente válidos** (**check obligatorio de autoría de era, loud-fail al cargar `EraDefinition`**): además de los checks por-knob de arriba, el cargador de `EraDefinition` **debe** verificar, para **cada** héroe de la porción de héroes del `unit_roster` de la era, que `ceil(hero_max_health / kaiju_attack_damage) × kaiju_attack_cooldown_s + telegraph_duration_s ≥ 30` (F2, OQ-11). Este es el caso peligroso que ningún check por-knob detecta: `telegraph_duration_s=14.0` (default, en rango) + `hero_max_health=120` (válido, dentro del `[120,200]` de Héroes) + kaiju MVP (`45`/`4.5`) da `time_to_death=27.5s < 30s` — rompe AC-H29 usando solo valores individualmente válidos de dos GDDs Approved. El check es **computable con datos ya disponibles al cargar la era** (`kaiju_definition` + porción de héroes del `unit_roster`, ambos anidados en `EraDefinition` y resueltos síncronamente por `ResourceLoader.load()`), así que se implementa como loud-fail, no como guía de autoría manual (resuelve OQ-11; ver AC-K74). +- **Config inválido cross-GDD — una sola tropa puede neutralizar (acoplamiento F4×F2)** (**check obligatorio de autoría de era, loud-fail al cargar `EraDefinition`**, salvo `allow_single_troop_neutralize=true`): el cargador **debe** verificar `kaiju_attack_damage × neutralize_hits_equivalent > telegraph_duration_s × max_single_troop_dps` (F4), donde `max_single_troop_dps` es el DPS de la tropa más letal del `unit_roster`. Con `neutralize_hits_equivalent=2` (`threshold=90`) y una tropa a 8 DPS, `90/8 = 11.25s < 12.5s` (piso de telegrafío) — una tropa sola neutraliza usando solo valores individualmente válidos. Mismo patrón cross-resource que el check de ≥30s; loud-fail salvo que la era declare explícitamente el flag opcional para permitirlo (ver F4, AC-K85). +- **Nota de implementación (Godot 4.6, godot-specialist) — el loud-fail NO puede usar `assert()` solo (corrección del 3er re-review)**: los loud-fail de arriba **no** deben derivarse de hints `@export_range` (clampa silenciosamente el slider del inspector y **no** lanza ante un valor fuera de rango puesto por script o en un `.tres` editado a mano — anularía en silencio "loud-fail, no clamp") **ni** de `_validate_property` (ese hook solo altera el `PropertyInfo`/hints del inspector; **no** rechaza un valor runtime malo). **Crítico**: `assert()` es un **no-op en builds exportados/release** (GDScript lo elimina fuera de debug), así que un setter que solo haga `assert(v > 0)` **no valida nada en el juego enviado** — reintroduce en silencio el bug que el loud-fail existe para evitar. El patrón correcto es un **setter de propiedad custom que use `push_error()` + un halt explícito** que dispare en debug **y** release, p. ej.: + ```gdscript + var x: float = 6.0: + set(v): + if v <= 0: + push_error("EncuentroConKaiju: x fuera de dominio (%s) — loud-fail" % v) + _config_invalid = true # marca la era como no-cargable; el call-site aborta la carga + x = v + ``` + (o `OS.crash()` / propagar un `Error` desde el `validate()` del call-site — nunca `assert()` a secas). El setter custom dispara tanto en asignación por script **como** en la deserialización de `.tres` (confirmado en 4.6: `ResourceLoader` escribe vía `Object::set()`, la misma ruta de dispatch que una asignación de script). El check cross-GDD de `EraDefinition` (arriba) encaja mejor como una función `validate()` dedicada llamada inmediatamente tras `ResourceLoader.load()` que retorne/propague un `Error` (no `assert`), ya que cruza campos de varios resources (kaiju + porción de héroes del `unit_roster`). **Centralizar toda carga de era en una única función** que llame `validate()` — Godot no tiene callback nativo "post-deserialize", así que un call-site que omita la llamada saltaría el check en silencio. +- **Furia (`ENRAGED`) durante `TELEGRAPHING`** (Regla 10/F5): si el kaiju enrage a mitad de un telegrafío en curso, el `telegraph_duration_s` efectivo se recalcula con `enrage_telegraph_factor` — si el tiempo ya transcurrido en el telegrafío actual ya supera el nuevo (más corto) `telegraph_duration_s_effective`, el telegrafío resuelve a `STRIKING` de inmediato (nunca se "revierte" tiempo ni se produce un valor negativo de tiempo restante). El daño del golpe usa `attack_damage_effective`. La Furia **no** reinicia el ciclo de intención ni limpia el marcado en curso. +- **Furia y prioridad de victoria/derrota**: `enraged` es un modificador, no un estado terminal — nunca compite con la Regla 9. Si la victoria o la derrota se cumplen mientras el kaiju está enraged, se aplican con la misma prioridad absoluta que fuera de Furia (el ciclo de intención termina de inmediato). +- **Una oleada de esbirros aterriza a mitad de un telegrafío del kaiju** (colisión `wave_interval_s` vs `telegraph_duration_s`): es posible y esperable (p. ej. `wave_interval_s=15` vs telegrafío de 14s casi garantiza solapamiento). No se bloquea ni se re-agenda el spawn — pero su efecto sobre la letalidad del héroe telegrafiado **sí** se refleja honestamente vía `incoming_damage_rate_total` (Regla 6b/F6), que suma el DPS de los nuevos esbirros enganchados. La lectura visual del telegrafío del kaiju no se ve comprometida porque el kaiju gana la jerarquía visual por defecto (art bible 3.4, KV-1) y los esbirros nunca ejecutan el lenguaje de fisuras del telegrafío (KV-7); el cap de concurrencia (Regla 8b) acota cuántos esbirros pueden competir por el espacio de pantalla. *(El tuneo fino de `wave_interval_s` relativo al ritmo de telegrafío es guía de autoría de encuentro — ver Tuning Knobs.)* +- **El objetivo telegrafiado muere o sale de `aggro_range` antes de que `TELEGRAPHING` complete**: el telegrafío se cancela — el kaiju vuelve a `SEEKING` sin pasar por `STRIKING` ni `STAGGERED` (esto no es una neutralización, Regla 7 — no se acredita `P=1` a nadie por esta vía). El marcado (`marked_target`/`marked_threat`, Regla 5) se limpia de inmediato al salir de `TELEGRAPHING`, igual que en cualquier otra salida del estado. +- **Empate exacto en `threat_score` entre dos o más candidatos** (posible pero improbable dado el término aleatorio continuo): desempate determinista por `entity_id` más bajo (el `int` estable autoral definido en el preámbulo de Formulas — **no** `get_instance_id()`) — nunca el primero evaluado ni una repetición del término aleatorio. Aplica el **mismo principio** de determinismo que coding-standards exige ("no random seeds"); *no* hereda una cláusula de Combate/Daño — aquel resuelve su selección de "más cercano" (AC-C06) sin cláusula de empate propia, así que este desempate por `entity_id` es una definición nueva de este sistema (corrección del 3er re-review de una cita previa imprecisa a AC-C06). +- **El kaiju cruza 0 de vida mientras está en `TELEGRAPHING`** (el mismo golpe que neutraliza el ataque, o un golpe no relacionado, también mata al kaiju): la victoria de la era (Regla 9) tiene prioridad absoluta — el ciclo de intención termina de inmediato sin resolver `STAGGERED` ni volver a `SEEKING`; no importa si el daño que cruzó 0 también cruzó `threat_neutralize_damage_threshold` en el mismo golpe, la victoria ya está declarada y el resto es irrelevante. +- **El último héroe muere (derrota, Regla 9) mientras el kaiju está en cualquier estado del ciclo de intención**: la derrota tiene la misma prioridad absoluta que la victoria — el ciclo de intención termina de inmediato (sin importar si estaba en `SEEKING`, `TELEGRAPHING`, `STRIKING` o `STAGGERED`); este sistema no seleccciona más objetivos ni telegrafía más ataques tras declarar la derrota, aunque queden tropas vivas en el campo. +- **Los esbirros nunca marcan** (Regla 4/5, exclusivo del kaiju): un héroe que tanquea golpes de esbirros, o que muere rodeado de esbirros sin que el kaiju haya telegrafiado nada contra él ni contra un aliado suyo, no genera `marked_target`/`marked_threat` por esa vía — su `P` solo puede resolverse por el ciclo de intención del kaiju. Es la aplicación directa del Player Fantasy ("dos escalas de peligro, nunca confundibles") a la clasificación de Héroes: los esbirros son amenaza real, pero no son *la* amenaza que Permadeath reconoce como propósito. +- **`STAGGERED` no aplica un multiplicador de daño**: es una ventana puramente táctica (el kaiju no selecciona objetivo ni telegrafía durante `stagger_duration_s`) — no introduce una excepción a la regla de "daño plano, sin multiplicadores" de Combate/Daño (Regla 5 de ese GDD). El kaiju sigue perdiendo vida a la tasa normal de `apply_damage` si es atacado durante `STAGGERED`; el "castigo" por neutralizar es la ventana de ataque libre sin represalia, no daño amplificado. +- **La cadencia-objetivo de oleadas (F3) es independiente de si los esbirros anteriores siguen vivos; el spawn real está acotado por `max_concurrent_esbirros` (Regla 8b)**: `wave_count(t)` (el conteo-objetivo) avanza puramente por tiempo transcurrido — una oleada anterior aniquilada no acelera la siguiente. Pero el **momento de instanciación real** sí depende del conteo de vivos: si el jugador no limpia y se alcanza `max_concurrent_esbirros`, las oleadas debidas se **difieren** hasta que haya espacio (F3, gate de concurrencia). Esto acota el peor caso de esbirros en pantalla y convierte "limpiar" en una palanca real — corrige la acumulación no acotada que el MVP original no contemplaba. +- **`time_to_death` no descuenta daño de esbirros simultáneo — pero `incoming_damage_rate_total` sí lo expone** (Formulas F2/F6, Regla 6b): `time_to_death` modela únicamente el daño del kaiju sobre el héroe telegrafiado (proyección honesta etiquetada "vs kaiju"). Para no mentirle al jugador sobre la muerte real (Pilar 4), este sistema expone **en paralelo** `incoming_damage_rate_total` (F6), que suma el DPS de todas las fuentes (kaiju + esbirros enganchados). El readout de Héroes (U-3) debe comparar ambos y reflejar la fuente más peligrosa — si la muerte por daño combinado llega antes que `time_to_death`, el readout no presenta `time_to_death` como vida garantizada. Esto **cierra** el hueco de honestidad que el MVP original dejaba abierto (antes era OQ-3, ahora resuelto). +- **`incoming_damage_rate_total` cuando el kaiju NO está enganchado al héroe (`kaiju_projected_dps=0`, F6)**: si un héroe recibe daño solo de esbirros (el kaiju telegrafía/ataca a otro objetivo), `kaiju_projected_dps(hero)=0` y `incoming_damage_rate_total = Σ esbirro_dps` — refleja honestamente el peligro solo-esbirros. En ese caso `time_to_death` (kaiju-only, F2) **no está definido** para ese héroe (el kaiju no lo telegrafía). El consumidor (readout de Héroes) **no debe** tratar un `time_to_death` ausente como "vida infinita/seguro": cuando `time_to_death` no aplica pero `incoming_damage_rate_total > 0`, la única proyección válida es `hero_HP_current / incoming_damage_rate_total`, y el readout debe usar esa. Nunca debe defaultear `time_to_death` ausente a `0` (falsa alarma) ni a `∞` (falso "a salvo"). Este sistema expone `time_to_death` como explícitamente "no aplica / sin telegrafío del kaiju" (no un número centinela) para forzar esta distinción en el consumidor. +- **Retirada táctica vs. desenganche sostenido (F5 disparador (b))**: una retirada de reposicionamiento normal (sacar un héroe de `aggro_range` unos segundos y volverlo a comprometer) **no** dispara Furia, porque (1) el piso de `hero_disengage_window_s` es 15s — una retirada más corta no vacía la ventana — y (2) la condición es consciente del roster: mientras la fracción requerida de héroes vivos siga golpeando dentro de la ventana, retirar a *uno* es libre. La Furia (b) solo se dispara cuando el jugador deja caer el número de héroes comprometidos por debajo de `required_engaged_heroes` de forma sostenida (turtle real o cebo-único), no por micro-retiradas tácticas. No hay estado que distinga "intención de retirada" de "abandono" — la ventana deslizante + la fracción de roster lo resuelven por construcción. +- **Una era con `max_waves=0` (sin esbirros, Regla 1/8)**: el ciclo de intención del kaiju y la evaluación de victoria/derrota funcionan exactamente igual — la ausencia de esbirros no requiere ninguna rama de código distinta en este sistema, solo un dato de era en 0 (ver F3). + +## Dependencies + +**Dependencias hacia arriba (upstream) — lo que este sistema necesita:** + +| Sistema | Tipo | Interfaz | Estado | +|---|---|---|---| +| Combate/Daño | Dura (bidireccional) | Este sistema usa el FSM de enganche y `apply_damage` sin modificarlos; sobreescribe la selección de objetivo (Regla 3) y el `leash_range` (Regla 2) solo para el kaiju; consume `CombatProfile` y F2/F3 para F2 de este GDD; expone `marked_targets`/`marked_threats` que Combate/Daño lee para `P` | ✅ Approved — **resuelve Combate OQ-5 y OQ-7** | +| Temporizador de Preparación/Ritual | Dura | Consume la señal `entered_climax` (dispara Regla 1) y `kaiju_timer_remaining_ratio` fijado en 0 durante `CLIMAX`; nunca escribe de vuelta | ✅ Approved — contrato ya confirmado por Temporizador AC-T03b | +| Datos de Era/Civilización | Dura (datos) | Lee `kaiju_definition` (stats del kaiju) y, si la era los define, roster/oleadas de esbirros y `max_concurrent_esbirros` de la era `ACTIVE`. **Además lee la porción de héroes del `unit_roster`** (para los checks de autoría de era loud-fail: garantía ≥30s de AC-K74 y acoplamiento F4×F2 de AC-K85 — ambos cruzan `kaiju_definition` + `unit_roster`, anidados en `EraDefinition` y resueltos síncronamente por `ResourceLoader.load()`) | ⚠️ In Design — la interfaz `kaiju_definition` que este sistema necesita ya está estable en ese GDD aunque el documento completo no esté Approved; los campos `max_concurrent_esbirros` y `allow_single_troop_neutralize` son **adiciones nuevas** al bloque de esbirros de `kaiju_definition` (ver OQ-7); la lectura de la porción de héroes del `unit_roster` para los checks de era es una dependencia de datos nueva de este re-review | +| TimeControl (servicio Core) | Dura (solo lectura, vía TimeControl) | Lee `game_time_paused` de TimeControl mientras el régimen efectivo sea `PAUSED` (Permadeath lo solicita en `DEATH_HOLD`); congela el ciclo de intención completo mientras el flag esté activo (Regla 11). Los relojes de tiempo de juego derivan de `TimeControl.game_tick` | ✅ Cerrado — ADR-0002 (Accepted 2026-08-21); dueño del flag = TimeControl, fuente del régimen = Permadeath | + +**Dependientes hacia abajo (downstream — sistemas que dependen de este):** + +| Sistema | Tipo | Interfaz | Estado | +|---|---|---|---| +| Sistema de Héroes | Dura | Consume `time_to_death` (kaiju-only, Regla 6/F2) **e** `incoming_damage_rate_total` (todas las fuentes, Regla 6b/F6) proyectados por héroe | ✅ Approved — **desbloquea sus ACs mockeadas AC-H29/AC-H30/U-3**; la señal F6 cierra el hueco de honestidad de daño de esbirros | +| Transición de Era | Dura | Consume el veredicto de victoria/derrota de la era (Regla 9); posee qué ocurre después (props de legado, avance narrativo) | *Sin GDD (contrato propuesto por este documento)* | +| UI/HUD | Dura | Consume el estado del ciclo de intención, el objetivo marcado y `time_to_death` proyectado para renderizar el telegrafío legible | *Sin GDD (contrato propuesto por este documento)* | +| Audio | Dura | Consume las transiciones del ciclo de intención (especialmente entrada a `TELEGRAPHING`) para cues de advertencia | *Sin GDD (contrato propuesto por este documento)* | + +**Nota cross-GDD (acción Phase 5)**: Combate/Daño, Temporizador de Preparación/Ritual y Sistema de Héroes todavía dicen "Encuentro con Kaiju — sin GDD (contrato propuesto)" en sus propias tablas de Dependencies/Interactions, y sus Mock Contract Assumptions bloqueadas (Combate AC-C32–C35, Héroes AC-H29/AC-H30/AC-H31b) siguen citando "sin GDD" como el bloqueo. Con este documento existiendo, esas notas quedan stale — se recomienda des-flaggearlas y re-verificar los mocks contra el contrato real (esta sección + Regla 3/5/6/9) en la fase de registro de este GDD, no editarlas silenciosamente ahora. + +## Tuning Knobs + +| Knob | Símbolo | Tipo | Default MVP | Rango seguro | Qué se rompe en extremos | +|---|---|---|---|---|---| +| Ventana de amenaza | `threat_window_s` | float (F1) | 6.0 | 3.0–12.0 | ≤0: loud-fail (F1 degenera, ignora daño reciente); muy corto: la puntuación de amenaza olvida daño reciente casi de inmediato, el kaiju se siente errático; muy largo: daño de hace mucho sigue pesando, el kaiju no reacciona a cambios de situación | +| Peso del daño | `threat_damage_weight` | float (F1) | 1.0 | 0.5–2.0 | Muy bajo: el bono de héroe domina por completo, el daño infligido deja de importar para la selección; muy alto: una tropa que golpeó mucho puede superar a un héroe recién llegado, diluye el sesgo héroe>tropa (Player Fantasy) | +| Bono de héroe | `hero_threat_bonus` | float (F1) | 100 | 50–200 | Muy bajo: el kaiju puede ignorar héroes en favor de tropas que le hicieron más daño, rompe "el kaiju viene por ti"; muy alto: el kaiju siempre prioriza héroes sin importar el daño real recibido, la amenaza se vuelve irrelevante | +| Rango de aleatoriedad | `threat_random_range` | float (F1) | 15 | 5–30 | Muy bajo: la selección se vuelve casi 100% determinista, patrón explotable (exactamente lo que la Regla 3 evita); muy alto: dos candidatos con amenaza muy distinta pueden invertirse, la selección se siente arbitraria | +| Duración del telegrafío | `telegraph_duration_s` | float (F2) | 14.0 | 12.5–17.0 | <12.0 exacto: rompe la garantía de ~30s de Héroes AC-H29 (constraint duro, no de feel); >17.0: la ventana del Guardián supera ~40s, erosiona "breve e intenso" del Pilar 3 | +| Duración de aturdimiento | `stagger_duration_s` | float (Regla 7) | 3.0 | 1.5–5.0 | <1.5: la recompensa por neutralizar se siente insignificante, socava el incentivo de interrumpir; >5.0: pausa demasiado la presión del kaiju, diluye "intenso" | +| Retraso de primera oleada | `first_wave_delay_s` | float (F3) | 10.0 | 5–20 | Muy bajo: esbirros aparecen casi al instante de `entered_climax`, sin respiro inicial; muy alto: la primera oleada llega tan tarde que el jugador puede terminar el kaiju antes de verla, esbirros se sienten irrelevantes | +| Intervalo entre oleadas | `wave_interval_s` | float (F3) | 20.0 | 15–40 | ≤0: loud-fail (F3 divide por 0 en el floor); muy corto: oleadas se acumulan más rápido de lo que se pueden limpiar, satura la pantalla; muy largo: los esbirros dejan de sentirse como presión continua | +| Golpes-equivalentes de neutralización | `neutralize_hits_equivalent` | int (F4) | 3 | 2–5 | ≤0: loud-fail (umbral trivial, cualquier golpe neutraliza); muy bajo (1): neutralizar es casi gratis, socava el telegrafío como amenaza real; muy alto (>5): neutralizar en la ventana de `telegraph_duration_s` deja de ser alcanzable ni en grupo, la Regla 7 se vuelve un no-op de facto. **Acoplado con `telegraph_duration_s`** (ver F4): la propiedad "una tropa sola no neutraliza" no se sostiene en todo el rango — elegir el par junto | +| Umbral de tiempo de Furia | `enrage_time_threshold_s` | float (F5) | 150.0 | 90–240 | ≤0: loud-fail; muy bajo: la Furia llega antes de que la pelea "normal" respire, el clímax se siente apresurado; muy alto: la cota de duración deja de morder, un jugador estancado sufre una pelea larguísima antes de la Furia (erosiona "breve" del Pilar 3) | +| Ventana de desenganche de héroes | `hero_disengage_window_s` | float (F5) | 15.0 | 15–30 | ≤0: loud-fail; **piso subido de 8 a 15**: por debajo de 15 una retirada táctica legítima de reposicionamiento dispara Furia injustamente (el default ahora vive en el piso); muy alto: el anti-turtle deja de morder — el jugador puede parquear héroes mucho tiempo sin castigo | +| Fracción de compromiso de roster | `hero_engagement_fraction` | float (F5) | 0.5 | 0.34–1.0 | ∉(0,1]: loud-fail; muy bajo (redondea a 1 héroe): vuelve al disparador "algún héroe" y reabre el exploit del héroe-cebo único; muy alto (1.0): exige que **todos** los héroes vivos golpeen cada ventana, castiga cualquier retirada táctica de un solo héroe. Acoplado con `hero_disengage_window_s` (ventana muy corta + fracción alta = anti-turtle hiperagresivo) | +| Factor de telegrafío en Furia | `enrage_telegraph_factor` | float (F5) | 0.6 | 0.4–0.8 | ∉(0,1]: loud-fail; muy bajo (0.4): telegrafíos tan cortos que la Furia se siente injusta/ilegible pese al aviso; muy alto (0.8): la Furia apenas cambia la presión, no fuerza resolución | +| Factor de daño en Furia | `enrage_damage_factor` | float (F5) | 1.5 | 1.0–2.5 | <1: loud-fail (invertiría el sentido de la Furia); 1.0: la Furia no sube letalidad (solo acorta telegrafío), puede no bastar para forzar resolución; muy alto (2.5): one-shots que rompen la legibilidad de `kill_time` incluso con aviso | + +**Datos por era (no knobs globales — viven en `kaiju_definition`, autorados por civilización):** + +| Dato | Tipo | Rango seguro sugerido | Descripción | +|---|---|---|---| +| Tope de oleadas | `max_waves` | int | 0–8 | 0 = era sin esbirros (Regla 1/8); por encima de 8 no se ha validado que el ritmo de `CLIMAX` lo sostenga | +| Tamaño de oleada | `esbirro_wave_size` | int | 1–6 | Unidades por oleada; escalar junto con `wave_interval_s` y `max_concurrent_esbirros` para no saturar | +| Tope de esbirros concurrentes vivos | `max_concurrent_esbirros` | int | 0–16 (recomendado); 17–24 = máximo técnico no validado | **Cota dura de esbirros en pantalla** (Regla 8b): al alcanzarse, las oleadas debidas se difieren hasta que baje el conteo. Acota el peor caso independiente de `max_waves × esbirro_wave_size`. 0 = sin esbirros (redundante con `max_waves=0`). Debe ser ≥ `esbirro_wave_size` para que una oleada quepa de una vez. **Rango recomendado bajado a 0–16** (observación de level-designer): 24 estaba documentado *en* el punto de saturación de pantalla, donde el kaiju pierde dominancia visual (art bible 3.4/Principio B, el pilar visual central del juego); 17–24 queda como máximo técnico a validar en playtest, no un valor seguro con margen. Sumar la presencia simultánea de tropas/héroes del jugador al presupuesto de pantalla al elegir este valor | + +**Interacción entre knobs** (cinco acoplamientos que la validación por-knob no detecta — el #1 ahora SÍ tiene enforcement cross-GDD obligatorio; los demás se re-verifican a mano/por guía de autoría al re-tunear): +1. **`telegraph_duration_s` — constraint duro (≥12.0s exacto) + check cross-GDD enforced**: es el único knob con un piso no-negociable de *feel* pre-Furia; sostiene la garantía de ≥30s de Héroes AC-H29. **Esa garantía además depende de `kaiju_attack_damage`/`attack_cooldown_s` de Combate/Daño** (F2) — que ese GDD trata como knobs tunables, no locked. **Restricción cross-GDD, ahora loud-fail obligatorio al cargar `EraDefinition`** (ya no solo prosa): `ceil(hero_max_health / kaiju_attack_damage) × kaiju_attack_cooldown_s + telegraph_duration_s ≥ 30` debe cumplirse para todo héroe de una era que enfrente al kaiju, verificado como check de datos (F2, Edge Cases "Config inválido cross-GDD", AC-K74; resuelve OQ-11). OQ-10 (formalizar el lock/trigger en Combate/Daño) sigue abierto. +2. **`threat_damage_weight` / `hero_threat_bonus` / `threat_random_range` acoplados (F1)**: cambiar uno sin revisar los otros dos puede invertir el sesgo héroe>tropa de la Regla 3 (punto de inversión cuantificado en F1). Re-verificar el sanity-check del ejemplo de F1 tras cualquier re-tuneo. +3. **`neutralize_hits_equivalent` × `telegraph_duration_s` (F4) — ahora enforced (AC-K85)**: la propiedad "una sola tropa no puede neutralizar en solitario" **no** se sostiene en todo el rango seguro (rota en los bordes — ver F4), así que dejó de ser guía de autor y es un loud-fail al cargar la era: `kaiju_attack_damage × neutralize_hits_equivalent > telegraph_duration_s × max_single_troop_dps` (la tropa **más letal** del roster; salvo flag `allow_single_troop_neutralize`). + +4. **`hero_engagement_fraction` × `hero_disengage_window_s` (F5, disparador (b))**: una ventana muy corta combinada con una fracción alta hace el anti-turtle hiperagresivo (castiga retiradas tácticas legítimas); una fracción que redondea a 1 héroe reabre el exploit del cebo-único. Elegir el par junto: para MVP, `0.5` + `15s` exige mantener ≥mitad del roster comprometido en ventanas de 15s sin castigar micro-retiradas. +5. **`first_wave_delay_s` ≥ `telegraph_duration_s` (regla de autoría de encuentro, F3 × F2)**: con MVP `first_wave_delay_s=10.0 < telegraph_duration_s=14.0`, la primera oleada de esbirros puede aterrizar *durante* el primer telegrafío que el jugador ve en su vida — saturando el momento de lectura más importante y frágil del encuentro (observación de level-designer). **Regla de autoría recomendada**: para el primer encuentro de un tipo de kaiju, `first_wave_delay_s ≥ telegraph_duration_s`, de modo que el jugador vea un ciclo de telegrafío limpio antes de que los esbirros compitan por su atención. No es un loud-fail (es guía de ritmo de encuentro, no una fórmula rota), pero sí una regla de autoría a respetar por defecto. + +Los knobs de Furia (`enrage_*`) no rompen garantías cross-sistema (la Furia suspende la garantía de ≥30s a propósito), pero `enrage_damage_factor` alto sí puede romper la legibilidad de `kill_time` — ajustar por feel dentro del rango seguro. **`enrage_telegraph_factor` — piso de legibilidad (observación de game-designer)**: en los bordes (`telegraph_duration_s=12.5 × enrage_telegraph_factor=0.4 → ~5.0s` efectivo) el telegrafío enraged puede comprimirse tanto que, contra un héroe ya dañado, roza lo ilegible pese al aviso — el Pilar 4 ("nunca emboscar") aplica en espíritu incluso donde la garantía de ≥30s se suspende. Recomendación: mantener `telegraph_duration_s_effective ≥ 6.0s` como piso de feel al elegir el par `telegraph_duration_s × enrage_telegraph_factor` para una era (no un loud-fail, pero sí una guía de autoría con el mismo estatus que los acoplamientos de F1/F4). + +**Enforcement**: loud-fail al cargar para todos los casos fuera de dominio marcados arriba (precedente de Combate/Daño y Temporizador; no clamp) — ver Edge Cases para el listado completo de condiciones inválidas, incluidos el **check cross-GDD obligatorio de la garantía de ≥30s** (autoría de era, F2/OQ-11/AC-K74) **y el check de acoplamiento F4×F2 "ninguna tropa sola neutraliza"** (F4, AC-K85). **Godot 4.6 (godot-specialist) — usar `push_error()` + halt, NUNCA `assert()` solo**: `assert()` es un no-op en builds exportados, así que un setter `set(v): assert(...)` **no valida en release** — implementar la validación con un **setter de propiedad custom que use `push_error()` + un flag/halt de configuración inválida** (dispara en asignación por script y en deserialización de `.tres`) o una función `validate()` post-`ResourceLoader.load()` que **propague un `Error`** — **no** vía `assert()`, `_validate_property` (solo altera el `PropertyInfo` del inspector) ni hints `@export_range` (clampa el slider, no lanza). Ver el ejemplo de código en Edge Cases. Los checks cross-GDD de era (≥30s, F4×F2) encajan como `validate()` post-carga (cruzan campos de varios resources); **centralizar toda carga de era en una única función** para que ningún call-site omita el check. + +**Constantes consumidas (no se duplican, MVP-fijadas — NO "locked" en Combate/Daño)**: `attack_damage`=45, `attack_cooldown_s`=4.5 del kaiju (del `CombatProfile` del kaiju de la era MVP, usadas en F2/F4/F5) — ver F2 para la advertencia cross-GDD sobre que estos son knobs tunables, no locked; HP de héroes por arquetipo 140/150/200 (de Sistema de Héroes, usadas en el sanity-check de F2; el rango válido completo es `[120,200]` — ver restricción de autoría de era en F2). + +## Visual/Audio Requirements + +*Referencia rectora: Art Bible (`design/art/art-bible.md`, Draft, 9/9 secciones) — Principios A/B/C (1.2), 2.2 (Clímax de Combate), 3.1/3.4 (silueta y jerarquía visual), 4.1/4.2/4.5 (sistema y seguridad de color), 5.1 (lenguaje material del kaiju, fisuras luminosas), 5.4/8.4 (filosofía y producción de LOD), 7.5 (mitigaciones de legibilidad no-visuales), 9.5/9.6 (Godzilla/Pacific Rim). A diferencia de Combate/Daño, este documento SÍ dispone de un art bible estable para citar — no es un contrato provisional. **Audio sigue siendo provisional** — sin audio bible ni dirección de audio aún, mismo estatus que Combate/Daño CA-1..3 y Héroes A-1..3.* + +### Visual + +**KV-1 — Telegraph, lectura macro: el cuerpo del kaiju es el reloj (art bible 5.1, 9.5).** Las fisuras/espinas del kaiju (lenguaje material "ídolo roto", 5.1) escalan en tres tercios explícitos a lo largo de `telegraph_duration_s`=14.0s, nunca como un disparo de un solo instante: **tercio 1 (0–4.7s)** — pulso apenas perceptible, brillo tenue y lento, apenas distinguible del lavado ambiental Acero Violeta-Ceniza (2.2); **tercio 2 (4.7–9.3s)** — el brillo se vuelve inequívoco, el pulso se acelera y se extiende por más superficie del cuerpo; **tercio 3 (9.3–14.0s)** — brillo saturado (dentro del rango acotado de Sangre Vieja, ver KV-4), pulso rápido, la extremidad/fauce que va a golpear se separa visualmente del resto del cuerpo. Porque el kaiju ya gana la jerarquía visual por defecto (art bible 3.4, Regla 1 — "nada rivaliza con la masa del kaiju"), esta escalación es visible desde cualquier zoom de cámara de la batalla, sin importar cuántas tropas ocupen la pantalla — resuelve la exigencia de legibilidad "a escala 24-32px con mucho clutter" citada en Combate/Daño. + +**Nota de diseño — el aviso fiable del tercio 1 vive en canales redundantes que NO exigen audio ni mirar un número (contra la promesa de "nunca emboscar")**: el **visual** del cuerpo del kaiju en el tercio 1 (0–4.7s) es deliberadamente sutil ("apenas distinguible del lavado ambiental") — para un jugador que atiende otros héroes/tropas, ese pulso **no** es un aviso fiable por sí solo. Esto es aceptable porque el aviso llega por **canales redundantes desde el frame 1**: +1. **Canal no-auditivo periférico (KV-1b, nuevo — corrección de accesibilidad del 3er re-review)**: al entrar a `TELEGRAPHING`, un **pulso de viñeta en el borde de pantalla** (registro Sangre Vieja, KV-4) escala con el mismo ritmo de tercios que el cuerpo del kaiju, **perceptible con los ojos en cualquier parte de la pantalla** sin depender de audio ni de mirar el HUD. En gamepad, se acompaña de **rumble** escalando en la misma curva. Este canal existe **desde el tercio 1** y es el aviso periférico primario para jugadores Sordos/con hipoacusia o que juegan en silencio (común en RTS) — cierra el hueco por el que apoyar el tercio 1 solo en el audio + número dejaba a esos jugadores perdiendo ~1/3 del telegrafío como aviso "ojos en otra parte". Sujeto al límite de fotosensibilidad y al toggle de destello reducido de KV-10 (la viñeta usa rampa de bloom sostenido, no parpadeo, bajo el toggle). +2. **Canal auditivo (KA-1)**: el audio, que este GDD exige perceptible "con los ojos en otra parte" desde el tercio 1 — redundante con (1), no sustituto. +3. **Canal numérico (`time_to_death`/`incoming_damage_rate_total`, Reglas 6/6b, readout de Héroes U-3)**: refuerzo, nunca el único aviso. + +El **visual del cuerpo del kaiju** inequívoco arranca en el tercio 2 (~9.3s del golpe con `telegraph_duration_s=14.0`), sumándose a los tres canales de arriba. La garantía de Pilar 4 descansa en **al menos un canal eyes-elsewhere que no exige audio** (la viñeta KV-1b) + audio + número — **ningún jugador queda dependiente de un solo sentido ni de mirar el HUD**. El tercio 1 comunica "el kaiju carga un golpe" por viñeta periférica + sonido; el tercio 2+ comunica "y va por *este* objetivo" por visión del cuerpo del kaiju. En Furia (KV-9) los tercios se comprimen pero se mantienen los tres, preservando este contrato en su forma acortada. *(La perceptibilidad real del audio del tercio 1 bajo mezcla densa sigue siendo un riesgo a validar en playtest — OQ-13 — pero ya no es el único backstop no-visual.)* + +**KV-2 — Telegraph, lectura micro: `marked_target` (vía defensiva, Regla 5).** El objetivo elegido gana una runa de suelo diegética (vocabulario de vitral emplomado, art bible 3.3/7.2 — no un ícono flotante genérico) que escala en el mismo ritmo de tercios que KV-1, en la misma familia Sangre Vieja. Alrededor, un anillo secundario más pálido y de menor valor marca `guard_radius` (=4.0, Héroes) — el área donde un héroe puede morir protegiendo y satisfacer `P=1` por la vía defensiva. La runa vive en el plano de suelo, no sobre la silueta de 24-32px de la unidad, para no competir con CV-1 de Combate/Daño (el destello de un frame por golpe) ni con la jerarquía de forma de troops-como-textura (3.4, Regla 3). + +**KV-3 — Telegraph, lectura direccional: `marked_threat` (vía ofensiva, Regla 5).** La amenaza en sí —el ataque telegrafiado— se lee en el propio kaiju: al entrar al tercio 3 (KV-1), la extremidad/fauce que va a golpear se orienta visiblemente hacia `marked_target`, dando una vía de intercepción legible al jugador que prefiere neutralizar (Regla 7) en vez de proteger. `marked_target` (KV-2) y `marked_threat` (KV-3) nunca necesitan un segundo código de color para distinguirse entre sí — su separación es puramente espacial (uno vive en el objetivo, el otro en el kaiju) y comparten el mismo ritmo de escalación (principio Gestalt de destino común), lo cual además refuerza que ambos son la misma amenaza vista desde dos ángulos tácticos, no dos eventos distintos. + +**KV-4 — Disciplina de color: Sangre Vieja escala, el Dorado nunca se toca.** Todo el ciclo de intención (KV-1 a KV-6) se construye exclusivamente sobre **Sangre Vieja** (`#7A1F2B`, art bible 4.1) escalando en valor/bloom/tasa de pulso/extensión — nunca introduce un segundo rojo ni roza el Oro Reliquia (`#F4C542`/`#FFE9A8`, exclusivo de reliquias/legado, Principio A). Esta es una extensión deliberada del uso semántico ya fijado en 4.2 ("Sangre Vieja = costo pagándose, ahora mismo") al dominio del telegrafío del kaiju — el test de diseño de Principio A ("¿esto existe porque un héroe murió?") sigue votando NO para el telegrafío, así que el dorado permanece intacto. La técnica de escalar por valor/movimiento en vez de por hue nuevo sigue directamente la recomendación explícita del propio art bible en 4.5 (fila "Sangre Vieja vs. futuro rojo crítico") — este sistema es el primer caso real de esa recomendación aplicándose, y se anota aquí como precedente para futuros sistemas. + +**KV-5 — `STRIKING`: impacto sin suavizar (resuelve/extiende Combate/Daño CV-4).** Al completar el tercio 3 sin interrupción, el pulso de KV-1 llega a su pico en un corte duro (no un fundido) en el instante exacto de contacto — el golpe en sí usa el CV-1 de Combate/Daño (destello de drenaje en la silueta del objetivo) sin modificarlo. Exclusivo de este sistema: un frame de impacto/hitstop breve (2-4 frames, escalado en magnitud según si el objetivo es héroe o tropa) acompaña únicamente los golpes de `STRIKING` del kaiju — nunca los golpes rutinarios de tropas/esbirros, preservando la restricción de "sin pausa de cámara" que Combate/Daño ya fijó para la muerte de tropa. Cada `STRIKING` no neutralizado añade una grieta permanente al motivo de "vitral quemado" del borde de pantalla ya establecido en el art bible 2.2 — un marcador acumulativo y persistente de cuánto ha costado este encuentro dentro de la era actual. + +**KV-6 — `STAGGERED`: la ventana se lee como una herida abierta, no como calma.** Al neutralizarse un ataque (Regla 7), el pulso de Sangre Vieja de KV-1 no se desvanece suavemente — se corta de golpe y las fisuras quedan visiblemente agrietadas/inertes ("ventilando" en vez de brillando), extendiendo el lenguaje de "ídolo roto" de 5.1 hacia su registro más dañado. Esto es deliberadamente distinguible de tres estados vecinos: del pulso ascendente de `TELEGRAPHING` (KV-1, que sube), del lavado ambiental frío constante de Acero Violeta-Ceniza que acompaña al kaiju en `SEEKING` (2.2, que no pulsa), y de la nada visual de un `STRIKING` recién resuelto. Este sistema expone el estado (`STAGGERED` + `stagger_duration_s` restante) para que UI/HUD renderice cualquier indicador adicional de cuenta regresiva — el límite es el mismo que Combate/Daño trazó en su CV-5: este documento fija el read diegético en el mundo, no el overlay de UI. + +**KV-7 — Esbirros vs. kaiju vs. tropas: la ausencia de telegrafío ES la señal (Player Fantasy, "dos escalas de peligro, nunca confundibles").** Los esbirros heredan la familia de forma quitinosa/angular y el color frío aplanado `#5A6B72` del kaiju en miniatura (art bible 3.1/4.2/5.1) — ya distinguibles de la astilla vertical Hueso de las tropas por forma y color simultáneamente (redundancia intencional para daltonismo, 4.5). Lo que este sistema añade: los esbirros **nunca** ejecutan KV-1 a KV-6 — no tienen fisuras, no escalan, no marcan (Regla 4/8, Edge Cases). Esto convierte la ausencia de telegrafío en una regla de lectura mecánicamente honesta: si algo golpea sin haber brillado primero, no es el kaiju — es estructuralmente imposible que lo sea. El jugador puede aprender esta regla sin UI adicional. + +**KV-8 — Victoria/Derrota: este sistema declara, no escenifica (handoff a Transición de Era).** Al cruce de la condición de Regla 9, el ciclo de intención se congela de inmediato (Edge Cases) sin resolver ningún estado visual pendiente de KV-1 a KV-6 — ningún fundido, ningún stinger propio. Para victoria, el art bible ya define el mood-state de destino (2.6, "alivio costoso... nunca triunfal", compuesto desde detrás de monumentos cicatrizados) que Transición de Era hereda. **Gap detectado**: el art bible Sección 2 no define un mood-state de Derrota (cubre 2.1 Preparación, 2.2 Clímax, 2.3 Muerte/Sacrificio, 2.4 Memorial, 2.5 Transición, 2.6 Victoria — ninguna para el caso donde muere el último héroe). Se recomienda añadirlo como Sección 2.7 cuando se autore el GDD de Transición de Era, no aquí — este documento solo posee el instante de la declaración, no su puesta en escena. + +**KV-9 — Furia (`ENRAGED`): un cambio de estado permanente y legible, nunca una emboscada (Regla 10, Pilar 4).** La entrada a `ENRAGED` debe leerse de inmediato y de forma persistente en el cuerpo del kaiju — a diferencia del pulso cíclico del telegrafío (KV-1, que sube y baja), la Furia es un **cambio de línea base irreversible**: las fisuras de Sangre Vieja (5.1) pasan a un estado de brillo/temperatura sostenidamente más alto que ya no vuelve a enfriarse entre telegrafíos, señalando que "las reglas cambiaron" para el resto del encuentro. Como los telegrafíos ya son más cortos en Furia (`enrage_telegraph_factor`), la escalación de tercios de KV-1 se comprime en el tiempo pero **mantiene los tres tercios** (nunca se salta el aviso — la Furia acorta la ventana, no la elimina; el Pilar 4 exige que incluso el golpe enraged se telegrafíe). Sigue exclusivamente sobre Sangre Vieja (KV-4, el Dorado nunca se toca). Este estado se expone a UI/HUD (flag `ENRAGED`) para cualquier indicador adicional; el read diegético en el mundo lo fija este documento. + +**KV-10 — Límite de fotosensibilidad del pulso/bloom (requisito de accesibilidad, observación de ux-designer).** La escalación de pulso y bloom de KV-1, **la viñeta de borde de KV-1b** y su forma comprimida en Furia (KV-9, con pulso más rápido) **deben** respetar un límite de tasa de destello de accesibilidad: el pulso luminoso nunca debe superar **3 destellos por segundo** en su fase más rápida (tercio 3, y tercio 3 comprimido en Furia), coherente con el umbral estándar de contenido intermitente que la checklist de accesibilidad del proyecto exige ("no flashing content without warning"). Si el ritmo de pulso derivado de `telegraph_duration_s_effective` en Furia cruzaría ese límite, la representación visual (cuerpo del kaiju **y** viñeta de KV-1b) debe expresar la urgencia por **intensidad/extensión de bloom sostenido** en vez de por frecuencia de parpadeo (los canales de audio de KA-1 y de rumble de KV-1b ya llevan la tasa de urgencia sin riesgo fotosensible). Además, debe existir una **opción de accesibilidad de destello reducido** que sustituya el pulso (del cuerpo **y** de la viñeta) por una rampa de brillo continua no parpadeante conservando la lectura de tres tercios — de modo que el canal periférico eyes-elsewhere de KV-1b siga funcionando incluso con el toggle activo. Esto es un requisito de diseño de este sistema, no diferible al GDD de Accesibilidad, porque nace de una mecánica que este documento define (la escalación del telegrafío). + +**KV-1b — Canal periférico no-auditivo del telegrafío (viñeta de borde + rumble, requisito de accesibilidad).** Definido en la Nota de diseño de KV-1: pulso de viñeta en el borde de pantalla (Sangre Vieja, KV-4) + rumble en gamepad, escalando en el mismo ritmo de tres tercios que KV-1 desde el tercio 1, perceptible con los ojos en cualquier parte de la pantalla y sin depender de audio. Es el aviso periférico primario para jugadores sin audio fiable; sujeto al límite de fotosensibilidad y toggle de KV-10. Su timing sigue las mismas fronteras de tercio que AC-K77 verifica (progreso continuo `[0.0,1.0]`, KU-1). + +### Audio *(provisional)* + +**KA-1 — Escalación audible del telegrafío, no solo un disparador de entrada. El audio es el canal de aviso fiable desde el tercio 1 (backstop de Pilar 4, no el número).** Un lecho sonoro (drone/pulso de baja frecuencia) escala en volumen y tasa de repetición en sincronía exacta con los tres tercios de KV-1 — el jugador debe poder sentir cuánto queda del telegrafío con los ojos en otra parte de la pantalla (crítico dado que el jugador gestiona tropas y otros héroes simultáneamente). **Requisito explícito de honestidad (resuelve la observación de ux-designer sobre KV-1)**: a diferencia del **visual** del tercio 1 (KV-1, deliberadamente sutil, "apenas distinguible del lavado ambiental"), el **audio** del tercio 1 **debe ser perceptible con los ojos en otra parte** desde el inicio mismo del telegrafío — es el canal que garantiza que el aviso más temprano no dependa de que el jugador esté mirando el readout numérico de Héroes. La promesa de "nunca emboscar" (Pilar 4) descansa en **dos canales diegéticos redundantes desde el frame 1** (audio de KA-1 perceptible + número de `time_to_death` en el readout), no en el número solo ni en el pulso visual sutil del tercio 1. El audio del tercio 1 no necesita ser fuerte, pero sí inequívocamente audible como "el kaiju está cargando un golpe" bajo la mezcla de combate. Refuerza directamente la mitigación de audio no-visual que el art bible ya exige en 7.5 para lectura de estado bajo presión. + +**KA-2 — Impacto de `STRIKING`: registro categóricamente mayor (extiende Combate/Daño CA-1).** El impacto del golpe resuelto es más grave/grande que cualquier sonido de combate rutinario, coherente con CA-1 ya fijado por Combate/Daño ("el golpe de kaiju es categóricamente más grave/grande"). No introduce un nuevo principio, solo confirma que este sistema no lo reabre. + +**KA-3 — `STAGGERED`: un sting distinto, no un silencio.** Al neutralizar, un sting resonante (tipo campana/cristal agrietado) marca el corte de KV-6, decayendo audiblemente a lo largo de `stagger_duration_s` — da al jugador una cuenta regresiva audible de la ventana de daño libre sin depender de leer el estado visual del kaiju constantemente. + +**KA-4 — Esbirros: registro intermedio, nunca el motivo del telegrafío.** El audio de esbirros (impacto/muerte) se ubica en timbre entre tropa y kaiju — frío, quitinoso, más cercano a la familia Acero — pero **nunca** reutiliza el motivo de escalación de KA-1: ese motivo debe permanecer exclusivo del kaiju para que sea reconocible sin mirar la pantalla (mismo argumento que KV-7). + +**KA-5 — Victoria/Derrota: handoff silencioso, no un verbo propio de este sistema (espejo de Combate/Daño CA-3).** Este sistema no dispara un stinger de "victoria"/"derrota" — al declarar el veredicto (Regla 9), el lecho de audio de combate se ducke y la notificación pasa a Transición de Era, igual que Combate/Daño ya cede la muerte de héroe a Permadeath en su propia CA-3. + +**KA-6 — Furia (`ENRAGED`): un cambio de lecho sonoro permanente, no un sting puntual (Regla 10).** La entrada a Furia marca un desplazamiento sostenido e irreversible del lecho de combate (registro más grave/denso, o una capa nueva que ya no se retira) — el jugador debe poder reconocer que el encuentro entró en Furia sin mirar la pantalla, coherente con KV-9 y con la mitigación no-visual que el art bible exige en 7.5. El motivo de escalación de telegrafío (KA-1) sigue existiendo pero ahora comprimido en el tiempo (telegrafíos más cortos); nunca se elimina el aviso audible del golpe. + +**Principios rectores**: A (art bible 4.2, drenaje/escalación de color — el dorado nunca se toca fuera de reliquias), B (2.2/3.4/9.5-9.6, el kaiju gana la jerarquía por defecto), C (2.3, el beat de muerte de héroe sigue siendo territorio de Héroes/Permadeath, este sistema no lo escenifica), 5.1 (lenguaje material del kaiju como fuente del telegrafío), 7.5 (redundancia no-visual para estado crítico bajo presión). + +📌 **Asset Spec** — Con Visual/Audio definido, se puede correr `/asset-spec system:encuentro-con-kaiju` para producir specs por-asset (escalación de fisuras del kaiju en 3 tercios, runa de suelo `marked_target`, VFX de `STAGGERED`, grieta persistente de pantalla). + +📌 **Art Bible Follow-up (no bloqueante, para registrar cuando se toque el art bible de nuevo)** — (1) confirmar formalmente en el art bible que Sangre Vieja escalando en valor/bloom es el vocabulario oficial de "amenaza del kaiju en curso" (hoy es una extensión razonada de 4.2/4.5, no una entrada explícita); (2) añadir un mood-state de Derrota (Sección 2.7) cuando se autore el GDD de Transición de Era — hoy el art bible solo cubre Victoria (2.6). + +## UI Requirements + +*Este sistema no diseña pantallas — expone datos que UI/HUD renderiza con el lenguaje diegético del art bible (Sección 7), en el límite ya trazado por Combate/Daño (CV-5/CU-1). Esta sección fija el contrato de qué datos fluyen, no cómo se ven.* + +**KU-1 — Exposición del ciclo de intención para UI/HUD.** Este sistema expone, en todo momento durante `CLIMAX`: el estado actual del ciclo de intención (`SEEKING`/`TELEGRAPHING`/`STRIKING`/`STAGGERED`), el flag `ENRAGED` (Regla 10), el tercio de telegrafío en curso si aplica (KV-1) **expuesto como progreso continuo `[0.0, 1.0]` dentro del telegrafío actual** (no solo un índice de tercio 1/2/3 — el pulso continuo de KV-1 lo requiere; el consumidor deriva el tercio si lo necesita), y la identidad de `marked_target`/`marked_threat` si hay un telegrafío activo. UI/HUD consume esto para cualquier overlay adicional sobre el read diegético ya fijado en Visual/Audio Requirements (KV-1..3, KV-9) — este sistema no decide si ese overlay existe ni cómo se ve. + +**Contrato de cancelación de marcado (observación de ux-designer — evitar que la runa desaparezca "como un bug")**: la salida de `TELEGRAPHING` expone **la razón** de la salida como parte del evento de limpieza de marcado, distinguiendo tres casos: (a) `STRIKING` (el golpe se resolvió — la runa culmina en impacto, KV-5), (b) `STAGGERED` (neutralización — la runa se corta a "herida abierta", KV-6), y (c) **cancelación** (el objetivo murió o salió de `aggro_range` antes de completar — Edge Cases/AC-K46). El caso (c) es el que un consumidor de UI/HUD necesita distinguir explícitamente para que la desaparición de la runa de suelo (KV-2) se lea como "la amenaza se disolvió", no como un fallo de render. Este sistema expone el enum de razón; cómo UI/HUD lo trata visualmente es del GDD de UI/HUD. + +**KU-2 — `time_to_death` (kaiju-only) e `incoming_damage_rate_total` (todas las fuentes) proyectados, consumidos por el readout de Héroes.** Este sistema expone por héroe **dos** señales: `time_to_death` (Fórmula F2, solo daño del kaiju, bajo telegrafío activo) e `incoming_damage_rate_total` (Fórmula F6, DPS combinado de kaiju + esbirros enganchados, en todo momento que el héroe esté bajo ataque). El consumidor principal es el readout de veredicto de Héroes (U-3, ya Approved), que **debe reflejar la fuente más peligrosa** — si la muerte por daño combinado (`hero_HP / incoming_damage_rate_total`) llega antes que `time_to_death`, el readout no debe presentar `time_to_death` como vida garantizada (cierra el hueco de honestidad del Pilar 4, Regla 6b). Este sistema solo provee los números proyectados en cada frame; cómo y cuándo Héroes los traduce en su indicador diegético (dorado incipiente sobre elemento asimétrico) es responsabilidad exclusiva de ese GDD, no de este. + +**Nota de flicker/histéresis en la señal dual (observación de ux-designer, para `/ux-design`)**: cuando la fuente más peligrosa **cambia** de frame a frame (p. ej. un esbirro enganchado muere y `incoming_damage_rate_total` cae de golpe, devolviendo el liderazgo de peligro al kaiju), la comparación de KU-2 puede oscilar. Este sistema expone los **valores crudos por frame** (`time_to_death` e `incoming_damage_rate_total` sin suavizar) — el suavizado/histéresis para evitar parpadeo del indicador es responsabilidad del consumidor (readout de Héroes / `/ux-design` del HUD del clímax), igual que Héroes ya contempla histéresis para su propio umbral de visibilidad (Héroes OQ-6). Se anota aquí explícitamente para que el autor del spec de UX lo decida (no se resuelve en silencio); ver OQ-14. + +**KU-3 — Sin editorializar el estado del encuentro.** Coherente con el understatement del juego y con Combate/Daño CU-4: este sistema no expone texto de "¡Cuidado!"/"¡Peligro!" ni juicios — solo estado numérico y eventos (ciclo de intención, `time_to_death`, marcados). La lectura de la amenaza es visual/sonora (KV-1..8, KA-1..5), no textual. + +**KU-4 — Evento de veredicto de era.** Al declararse victoria o derrota (Regla 9), este sistema emite un evento discreto único (no un stream continuo) que Transición de Era y UI/HUD consumen. Este sistema no posee la pantalla de resultado, la narrativa de consecuencia ni ningún tratamiento visual sostenido del veredicto (ver KV-8) — solo el disparo del evento en el instante exacto del cruce. + +📌 **UX Flag — Encuentro con Kaiju**: Este sistema alimenta UI de combate de alto riesgo de legibilidad (telegrafío, marcados, cuenta regresiva de `time_to_death`, indicador de `STAGGERED`). En Pre-Producción, correr `/ux-design` para el HUD del clímax **antes** de escribir epics; las stories que referencien esa UI deben citar el spec de UX, no este GDD. + +## Acceptance Criteria + +*Convención: cada AC usa formato Dado/Cuando/Entonces y una etiqueta de tipo de evidencia (`[Logic/unit]`, `[Integration]`). Los AC marcados **(bloqueado)** dependen de un sistema — o de una porción no escrita de un sistema — sin contrato confirmado hoy; solo verificables mediante un mock en el límite de este sistema. Salvo indicación, los valores de ejemplo usan `telegraph_duration_s=14.0`, `threat_neutralize_damage_threshold=135`, y los tres arquetipos ilustrativos de Héroes (Vanguardia 140 HP / Portaestandarte 150 HP / Guardián 200 HP).* + +### A. Instanciación y persecución (Reglas 1-2) + +**AC-K01a — Sin kaiju antes de `entered_climax`** **[Integration]** +Dado el estado del juego en cualquier punto anterior a la señal `entered_climax` de Temporizador, cuando se inspecciona la escena, entonces no existe ninguna instancia del kaiju. + +**AC-K01b — Kaiju instanciado en el frame de `entered_climax`** **[Integration]** +Dado que la señal `entered_climax` se recibe, cuando se procesa, entonces el kaiju se instancia en ese mismo frame. + +**AC-K02 — `CombatProfile` del kaiju poblado exclusivamente desde `kaiju_definition`, sin hardcode** **[Logic/unit]** +Dado un `kaiju_definition` con `attack_damage`, `attack_range`, `attack_cooldown_s`, `aggro_range` poblados para la era `ACTIVE`, cuando el kaiju se instancia, entonces su `CombatProfile` usa exclusivamente esos valores — cambiar el resource cambia el comportamiento sin tocar código. + +**AC-K03 — Esbirros no se instancian antes de `entered_climax`** **[Logic/unit]** +Dado una era con `max_waves > 0`, cuando el estado del juego es cualquiera anterior a `CLIMAX`, entonces cero esbirros existen instanciados en la escena, sin importar el valor de `first_wave_delay_s`. + +**AC-K04 — Contrato unidireccional: sin escritura hacia Temporizador** **[Integration]** +Dado el ciclo completo de `CLIMAX` (cualquier transición del ciclo de intención, oleada de esbirros, o declaración de victoria/derrota), cuando se audita qué métodos de Temporizador invoca este sistema, entonces ninguno — este sistema solo lee `entered_climax` y `kaiju_timer_remaining_ratio`, nunca escribe de vuelta (contrato ya confirmado por Temporizador AC-T03b). + +**AC-K05a — Sin leash: persecución ilimitada por distancia** **[Integration]** +Dado un kaiju enganchado a un objetivo que se aleja más allá de la distancia que activaría un retorno a posición de retención en cualquier otra unidad, cuando se evalúa si debe abandonar la persecución, entonces continúa persiguiendo — nunca transiciona a `NO_TARGET` por distancia recorrida. + +**AC-K05b — El `leash_range` del kaiju nunca se lee** **[Integration]** +Dado un `CombatProfile` de kaiju con `leash_range` poblado con cualquier valor numérico (ya no requiere ser el centinela `provisional*` de Combate/Daño AC-C27b), cuando el kaiju persigue, entonces ese campo nunca se lee — Regla 2 anula el campo por completo, no solo lo exime de la validación F5. + +**AC-K06 — Fin de persecución: solo tres causas válidas** **[Logic/unit]** +Dado un kaiju enganchado a un objetivo, cuando deja de perseguirlo, entonces la causa es exactamente una de: (a) el objetivo murió, (b) el objetivo salió de `aggro_range` sin otro candidato válido cercano, o (c) este sistema seleccionó un nuevo objetivo vía Regla 3 — ninguna otra causa (p. ej. distancia recorrida) es válida. + +### B. Selección de objetivo y ciclo de intención (Reglas 3-4, 8) + +**AC-K07 — Selección por amenaza sobreescribe aggro por cercanía, solo para el kaiju** **[Logic/unit]** +Dado el kaiju sin objetivo y ≥2 candidatos válidos a distintas distancias con distinto `threat_score` (Fórmula F1), cuando selecciona objetivo, entonces elige el de mayor `threat_score` — no el más cercano, aunque el más cercano tenga menor amenaza. + +**AC-K08 — Esbirros usan aggro estándar de Combate/Daño (más cercano), no F1** **[Integration]** +Dado un esbirro sin objetivo y ≥2 candidatos válidos a distintas distancias, cuando selecciona objetivo, entonces elige el más cercano (Regla 4 de Combate/Daño) — F1 nunca se evalúa para esbirros. + +**AC-K09 — Recálculo de amenaza se dispara solo al completar `STRIKING` o al perder el objetivo actual** **[Logic/unit]** +Dado un kaiju en `TELEGRAPHING` o `STRIKING` con un objetivo válido, cuando se evalúa si debe recalcular `threat_score`, entonces no lo hace — el recálculo solo ocurre al entrar a `SEEKING` (fin de `STRIKING`, fin de `STAGGERED`, o pérdida del objetivo). + +**AC-K10 — Ciclo de intención completo sin interrupción: `SEEKING→TELEGRAPHING→STRIKING→SEEKING`** **[Logic/unit]** +Dado un kaiju en `SEEKING` que selecciona un objetivo y ningún golpe cruza `threat_neutralize_damage_threshold` durante el telegrafío, cuando transcurre `telegraph_duration_s` completo, entonces transiciona `TELEGRAPHING→STRIKING`, resuelve el golpe vía Combate/Daño, y vuelve a `SEEKING` — nunca pasa por `STAGGERED` en este camino. + +**AC-K11 — `STAGGERED` solo alcanzable vía neutralización exitosa** **[Logic/unit]** +Dado el kaiju en cualquier estado del ciclo de intención, cuando se inspecciona cómo se llega a `STAGGERED`, entonces la única transición entrante es `TELEGRAPHING→STAGGERED` vía Regla 7 — `SEEKING` y `STRIKING` nunca transicionan directamente a `STAGGERED`. + +**AC-K12 — Telegrafío exclusivo del kaiju: esbirros atacan sin telegrafío** **[Integration]** +Dado un esbirro con objetivo adquirido dentro de `attack_range`, cuando su cooldown expira, entonces ataca de inmediato con el ciclo estándar de Combate/Daño (AC-C04) — nunca entra a una fase de telegrafío. + +**AC-K13 — Esbirros: `CombatProfile` propio, mismo schema, sin compartir el ciclo de intención** **[Logic/unit]** +Dado un tipo de esbirro con su propio `CombatProfile` poblado, cuando se instancia y combate, entonces usa exclusivamente sus propios valores (schema idéntico a Combate/Daño) y en ningún momento entra a `SEEKING`/`TELEGRAPHING`/`STRIKING`/`STAGGERED` — ese ciclo es propiedad exclusiva del kaiju. + +### C. Fórmula F1 — Puntuación de amenaza (Regla 3) + +**AC-K14 — F1 exacto: héroe con daño reciente gana determinísticamente** **[Logic/unit]** +Dado `threat_window_s=6.0`, `threat_damage_weight=1.0`, `hero_threat_bonus=100`, `threat_random_range=15`, con Vanguardia (héroe) habiendo infligido 40 de daño reciente, un `melee_infantry` con 24, y un `support_troop` con 0, cuando se calcula `threat_score` para cada uno, entonces se cumplen **las tres** aserciones (cada una verificable de forma independiente): (a) `threat_score(Vanguardia) ∈ [125,155]`; (b) `threat_score(melee_infantry) ∈ [9,39]`; (c) `threat_score(support_troop) ∈ [-15,15]`. El resultado agregado esperado es que Vanguardia gana de forma determinista (su mínimo `125` supera el máximo `39` de cualquier otro). *(Nota de testeo: la RNG de `random_variation` se inyecta seedeada — ver AC-K71 — para que los tres rangos sean verificables sin flakiness.)* + +**AC-K15 — F1 sin historial de daño: el héroe gana por el bono fijo** **[Logic/unit]** +Dado un héroe y ≥1 tropa presentes al inicio de `CLIMAX`, ninguno habiendo dañado aún al kaiju, cuando se calcula `threat_score` con los valores MVP, entonces `threat_score(héroe) ∈ [85,115]` y `threat_score(tropa) ∈ [-15,15]` — el héroe gana de forma determinista pese a no tener historial de daño. + +**AC-K16 — F1 con solo tropas presentes: selección resuelta por el término aleatorio, sin fallback por cercanía** **[Logic/unit]** +Dado únicamente candidatos tropa (ningún héroe) en rango, todos con `recent_damage_dealt` igual (incluyendo 0), cuando se calcula `threat_score` para cada uno, entonces todos caen en el mismo rango `[-15,15]` y la selección la resuelve exclusivamente `random_variation` — el sistema nunca introduce un fallback silencioso por distancia. + +**AC-K17a — F1: cota inferior en `-threat_random_range`** **[Logic/unit]** +Dado cualquier combinación válida de `recent_damage_dealt ≥ 0` y `is_hero ∈ {0,1}`, cuando se calcula `threat_score`, entonces el resultado nunca es menor a `-threat_random_range`. + +**AC-K17b — F1: sin cota superior** **[Logic/unit]** +Dado `recent_damage_dealt` creciente sin límite, cuando se calcula `threat_score`, entonces no existe ningún techo — el valor crece monótonamente con `recent_damage_dealt`. + +**AC-K18 — F1 loud-fail: `threat_window_s ≤ 0`** **[Logic/unit]** +Dado un knob `threat_window_s=0` o negativo, cuando el sistema carga la configuración, entonces falla ruidosamente — nunca se ejecuta F1 con una ventana degenerada. + +### D. Fórmula F2 — Proyección `time_to_death` (Regla 6, contrato con Héroes) + +**AC-K19 — F2 exacto: Vanguardia a HP completo, `telegraph_duration_s=14.0`** **[Logic/unit]** +Dado Vanguardia (140 HP) a HP completo al entrar a `TELEGRAPHING` contra el kaiju (`attack_damage=45`, `attack_cooldown_s=4.5`), cuando se calcula F2, entonces `kill_time_hits_kaiju=4`, `kill_time_hits_kaiju × attack_cooldown_s_kaiju=18.0s`, y `time_to_death=32.0s` exactamente. + +**AC-K20 — F2 exacto: Guardián a HP completo, `telegraph_duration_s=14.0`** **[Logic/unit]** +Dado el Guardián (200 HP) a HP completo bajo las mismas condiciones, cuando se calcula F2, entonces `kill_time_hits_kaiju=5`, `kill_time_hits_kaiju × attack_cooldown_s_kaiju=22.5s`, y `time_to_death=36.5s` exactamente. + +**AC-K21 — Constraint duro: `telegraph_duration_s=12.0` es el mínimo exacto que sostiene ≥30s para el peor caso** **[Logic/unit]** +Dado el héroe de menor TTK crudo del roster ilustrativo (Vanguardia/Portaestandarte, TTK crudo 18.0s de Combate/Daño F3), cuando `telegraph_duration_s=12.0` exacto, entonces `time_to_death=30.0s` exactamente — el límite inclusivo de la garantía de Héroes AC-H29. + +**AC-K22 — `telegraph_duration_s < 12.0` rompe la garantía de Héroes AC-H29 (boundary negativo)** **[Logic/unit]** +Dado `telegraph_duration_s=11.9` (fuera del rango seguro `[12.5,17.0]` pero usado como boundary matemático) contra el héroe de menor TTK crudo, cuando se calcula F2, entonces `time_to_death=29.9s < 30.0s` — confirma por qué el rango seguro del knob excluye cualquier valor bajo 12.5. + +**AC-K23 — `hero_HP_at_telegraph_start` se snapshotea, no se re-lee durante el telegrafío** **[Logic/unit]** +Dado un héroe cuyo `health_current` cambia (por curación o daño de esbirros) después de entrar a `TELEGRAPHING` pero antes de que el golpe se resuelva, cuando se lee `time_to_death`, entonces el valor usado en F2 sigue siendo el snapshoteado al inicio de `TELEGRAPHING`, no el `health_current` actual. + +**AC-K24 — La garantía de ≥30s NO se sostiene para un héroe ya dañado (caveat explícito, no un piso universal)** **[Logic/unit]** +Dado un héroe a HP crítico tal que `kill_time_hits_kaiju=1`, cuando entra a `TELEGRAPHING` con `telegraph_duration_s=14.0`, entonces `time_to_death=18.5s < 30s` — comportamiento esperado y documentado, no un bug; no existe una promesa de ≥30s para un héroe casi muerto. + +**AC-K25 — `time_to_death` no descuenta daño simultáneo de esbirros** **[Logic/unit]** +Dado un héroe bajo `TELEGRAPHING` del kaiju que recibe daño adicional de un esbirro en paralelo, cuando se lee `time_to_death`, entonces el valor expuesto modela únicamente el daño del kaiju — no se recalcula ni se reduce por el daño de esbirros, aunque la muerte real del héroe pueda ocurrir antes de lo proyectado. + +**AC-K26 — Contrato resuelto con Héroes: `time_to_death` por héroe consumido por AC-H29/U-3/AC-H32b** **[Integration]** +Dado `time_to_death` expuesto por este sistema para un héroe bajo `TELEGRAPHING`, cuando Héroes (ya Approved) lo consume para su trigger de visibilidad del readout, entonces el valor recibido en ese frame coincide exactamente con el proyectado por F2 — mismo patrón de cross-check sin mock que Combate/Daño AC-C50 (el lado receptor ya está diseñado). + +### E. Fórmula F3 — Cadencia de oleadas de esbirros (Regla 8) + +**AC-K27 — F3 exacto: antes de `first_wave_delay_s`, `wave_count=0`** **[Logic/unit]** +Dado `first_wave_delay_s=10.0`, cuando `climax_elapsed_s=5.0`, entonces `wave_count=0` — cero esbirros instanciados. + +**AC-K28 — F3 exacto: secuencia completa del ejemplo canónico** **[Logic/unit]** +Dado `first_wave_delay_s=10.0`, `wave_interval_s=20.0`, `max_waves=4`, `esbirro_wave_size=3`, cuando se evalúa `wave_count(t)` en `t=10s, 25s, 30s, 200s`, entonces los resultados son exactamente `1, 1, 2, 4` (el último clampado al tope). + +**AC-K29 — F3 monótono no decreciente y nunca negativo** **[Logic/unit]** +Dado cualquier par `t1 < t2` con los mismos knobs válidos, cuando se calcula `wave_count(t1)` y `wave_count(t2)`, entonces `wave_count(t2) ≥ wave_count(t1)` y ambos son `≥ 0`. + +**AC-K30 — F3 boundary-safe: `max_waves=0` produce `wave_count(t)=0` para todo `t`, sin rama de código separada** **[Logic/unit]** +Dado una era con `max_waves=0`, cuando se evalúa `wave_count(t)` para cualquier `t ≥ 0`, entonces el resultado es siempre `0` — el sistema no requiere ninguna condición especial de "era sin esbirros" fuera del clamp. + +**AC-K31 — Contrato de spawn: solo se instancia el delta de oleadas nuevas** **[Logic/unit]** +Dado `last_spawned_wave_count=1` y `wave_count(t)=2` en la evaluación actual, cuando el sistema de spawn procesa el tick, entonces instancia exactamente `(2−1) × esbirro_wave_size` unidades — nunca `wave_count(t) × esbirro_wave_size` (que re-spawnearía la oleada 1 ya instanciada). + +**AC-K32 — F3 loud-fail: `wave_interval_s ≤ 0`** **[Logic/unit]** +Dado `wave_interval_s=0` o negativo, cuando el sistema carga la configuración, entonces falla ruidosamente (evita división por cero en el `floor`). + +**AC-K33 — F3 independiente de si esbirros de oleadas anteriores siguen vivos** **[Logic/unit]** +Dado una oleada anterior completamente aniquilada y otra oleada anterior mayormente viva, cuando se evalúa `wave_count(t)` para la siguiente oleada, entonces el resultado depende únicamente de `t` — ninguno de los dos escenarios acelera ni retrasa la cadencia. + +### F. Fórmula F4 y Regla 7 — Neutralización de telegrafío + +**AC-K34 — F4 exacto: `threshold` MVP = 135** **[Logic/unit]** +Dado `kaiju_attack_damage=45` (locked) y `neutralize_hits_equivalent=3` (MVP), cuando se calcula `threat_neutralize_damage_threshold`, entonces el resultado es exactamente `135`. + +**AC-K35 — Contador de daño acumulado resetea a 0 en cada transición `SEEKING→TELEGRAPHING`** **[Logic/unit]** +Dado un kaiju que entra a un nuevo `TELEGRAPHING`, cuando la transición ocurre, entonces el contador de daño acumulado hacia `threat_neutralize_damage_threshold` se reinicia a `0`, sin importar el valor acumulado en la fase anterior. + +**AC-K36 — Daño residual de fase anterior no cuenta hacia la neutralización actual (anti-exploit)** **[Logic/unit]** +Dado daño infligido al kaiju durante `SEEKING`, `STRIKING`, o un `TELEGRAPHING` previo contra un objetivo distinto, cuando el kaiju entra a un nuevo `TELEGRAPHING`, entonces ese daño residual no cuenta hacia el umbral de neutralización del telegrafío actual — mismo principio anti-exploit que Combate/Daño AC-C43b. + +**AC-K37 — Neutralización exitosa cancela `STRIKING` y transiciona a `STAGGERED`** **[Logic/unit]** +Dado un kaiju en `TELEGRAPHING` cuyo daño acumulado (desde el inicio de ese `TELEGRAPHING`) alcanza `threat_neutralize_damage_threshold`, cuando se cruza el umbral, entonces el kaiju no llega a `STRIKING` — transiciona directamente a `STAGGERED` por `stagger_duration_s`. + +**AC-K38 — Acreditación de neutralización, incluso en mutual kill** **[Integration]** +Dado el combatiente cuyo golpe cruza `threat_neutralize_damage_threshold`, cuando ese mismo golpe resulta además en la muerte del combatiente por un efecto simultáneo, entonces se acredita como quien neutralizó la amenaza marcada (satisface `P=1` vía ofensiva si es un héroe, Regla 5) — coherente con el contrato de mutual kill de Combate/Daño AC-C42. + +**AC-K39 — F4 loud-fail: `neutralize_hits_equivalent ≤ 0`** **[Logic/unit]** +Dado `neutralize_hits_equivalent=0` o negativo, cuando el sistema carga la configuración, entonces falla ruidosamente — un umbral de 0 rompería la Regla 7 (cualquier golpe, incluso accidental, cancelaría el ataque). + +**AC-K40 — F4 sanity: un `melee_infantry` solo NO puede neutralizar dentro de la ventana** **[Logic/unit]** +Dado `threshold=135` y un único `melee_infantry` (8 DPS) atacando al kaiju durante todo `TELEGRAPHING`, cuando se calcula el tiempo requerido, entonces `135/8≈16.9s`, que excede `telegraph_duration_s=14.0s` — no puede neutralizar en solitario. + +**AC-K41 — F4 sanity: un héroe solo (Vanguardia) SÍ puede neutralizar dentro de la ventana** **[Logic/unit]** +Dado `threshold=135` y Vanguardia (27.8 DPS) atacando al kaiju durante todo `TELEGRAPHING`, cuando se calcula el tiempo requerido, entonces `135/27.8≈4.9s`, cómodamente dentro de `telegraph_duration_s=14.0s`. + +**AC-K42 — `STAGGERED` no aplica multiplicador de daño** **[Logic/unit]** +Dado el kaiju en `STAGGERED` recibiendo golpes, cuando se aplica `apply_damage`, entonces la vida decrece a la tasa plana normal de Combate/Daño — sin ningún multiplicador por estar aturdido. + +### G. Marcado (Regla 5) — contrato `P` con Combate/Daño + +**AC-K43 — Entrada a `TELEGRAPHING` marca `marked_target` (vía defensiva) — resuelve Combate/Daño AC-C32** **[Integration]** +Dado un kaiju que transiciona `SEEKING→TELEGRAPHING` contra un objetivo Y, cuando la transición ocurre, entonces Y se convierte en `marked_target`, y cualquier héroe que muera dentro de `guard_radius` de Y mientras el marcado esté activo satisface `P=1` vía defensiva. + +**AC-K44 — Entrada a `TELEGRAPHING` marca `marked_threat` (vía ofensiva) — resuelve Combate/Daño AC-C33** **[Integration]** +Dado la misma transición, cuando el ataque telegrafiado se convierte en `marked_threat`, entonces un héroe que lo neutraliza (Regla 7) dentro de esa ventana satisface `P=1` vía ofensiva — mismo peso que la vía defensiva. + +**AC-K45 — Marcado se limpia al salir de `TELEGRAPHING` por cualquier vía, no persiste entre telegrafíos** **[Logic/unit]** +Dado un `TELEGRAPHING` que termina (por `STRIKING`, por neutralización, o por cancelación edge-case), cuando la transición de salida ocurre, entonces `marked_target`/`marked_threat` se limpian de inmediato — el siguiente `TELEGRAPHING` empieza sin marcado heredado. + +**AC-K46 — Telegrafío cancelado por muerte/salida de rango del objetivo NO acredita `P=1`** **[Logic/unit]** +Dado un objetivo telegrafiado que muere o sale de `aggro_range` antes de que `TELEGRAPHING` complete, cuando el kaiju vuelve a `SEEKING` sin pasar por `STRIKING` ni `STAGGERED`, entonces nadie se acredita `P=1` por esta vía — se distingue explícitamente de una neutralización (Regla 7). + +**AC-K47 — Los esbirros nunca marcan** **[Logic/unit]** +Dado un héroe que muere rodeado de esbirros sin que el kaiju haya telegrafiado nada contra él ni contra un aliado suyo, cuando se computa `P`, entonces ningún `marked_target`/`marked_threat` existe atribuible a esbirros — `P` solo puede resolverse por el ciclo de intención del kaiju. + +### H. Edge cases adicionales y Victoria/Derrota (Regla 9) + +**AC-K48 — Empate en `threat_score`: desempate determinista por `entity_id` más bajo** **[Logic/unit]** +Dado ≥2 candidatos con `threat_score` exactamente igual, cuando se resuelve la selección, entonces se elige el de `entity_id` numéricamente más bajo — donde `entity_id` es el `int` estable asignado por el contador monótono de spawn del encuentro (**no** `get_instance_id()` de Godot, que no es reproducible entre corridas), nunca el primero evaluado ni una repetición del término aleatorio. *(Este desempate es una definición propia de este sistema; Combate/Daño AC-C06 no define una cláusula de empate — ver preámbulo de Formulas, `entity_id`.)* + +**AC-K49 — Victoria: el kaiju cruza 0 de vida declara la victoria de la era en ese instante** **[Integration]** +Dado el kaiju cuya vida cruza 0 (notificado por `apply_damage` de Combate/Daño), cuando se procesa el cruce, entonces este sistema declara la victoria de la era en ese mismo frame. + +**AC-K50 — El kaiju cruza 0 durante `TELEGRAPHING`: la victoria tiene prioridad absoluta** **[Integration]** +Dado un golpe que cruza 0 la vida del kaiju en el mismo frame en que también cruza `threat_neutralize_damage_threshold` de un `TELEGRAPHING` activo, cuando se procesa, entonces el ciclo de intención termina de inmediato sin resolver `STAGGERED` ni volver a `SEEKING` — la victoria se declara y el resto es irrelevante. + +**AC-K51 — Derrota: el último héroe vivo muere declara la derrota en ese instante** **[Integration]** +Dado que ningún héroe de la civilización activa permanece en un estado `ALIVE_*`, cuando la última muerte se procesa, entonces este sistema declara la derrota de la era en ese mismo frame. + +**AC-K52 — La derrota tiene prioridad absoluta sobre cualquier estado del ciclo de intención** **[Integration]** +Dado el kaiju en cualquier estado (`SEEKING`/`TELEGRAPHING`/`STRIKING`/`STAGGERED`) en el momento en que se cumple la condición de derrota, cuando se procesa, entonces el ciclo de intención termina de inmediato — este sistema no selecciona más objetivos ni telegrafía más ataques tras declarar la derrota, aunque queden tropas vivas. + +**AC-K53 — Las tropas no son condición de derrota** **[Logic/unit]** +Dado una civilización sin héroes vivos pero con ≥1 tropa viva, cuando se evalúa la condición de derrota, entonces se cumple igualmente — la presencia de tropas vivas no la impide. + +**AC-K54 — Config inválido genérico: `telegraph_duration_s` fuera de `[12.5, 17.0]` → loud-fail** **[Logic/unit]** +Dado `telegraph_duration_s` fuera del rango seguro (incluyendo cualquier valor `< 12.0` exacto, que rompería el constraint duro de Héroes), cuando el sistema carga la configuración, entonces falla ruidosamente — nunca se clampa. + +### I. Exposición de datos downstream (UI/HUD, Audio, Transición de Era) + +**AC-K55 — Exposición del estado del ciclo de intención para consumidores (KU-1)** **[Integration]** +Dado `CLIMAX` en curso, cuando se consulta el estado expuesto por este sistema, entonces incluye en todo momento el estado actual del ciclo de intención, el tercio de telegrafío en curso si aplica, y la identidad de `marked_target`/`marked_threat` si hay un telegrafío activo — verificable con un suscriptor mock, sin requerir que UI/HUD exista. + +**AC-K56 — Sin editorializar: el payload expuesto no contiene texto de juicio (KU-3)** **[Logic/unit]** +Dado cualquier evento o dato expuesto por este sistema (estado del ciclo, `time_to_death`, marcados, veredicto), cuando se inspecciona su payload, entonces no contiene ningún campo de texto tipo "¡Cuidado!"/"¡Peligro!" ni cadena editorializada — solo estado numérico/enumerado y eventos discretos. + +**AC-K57 — Evento de veredicto de era: único y discreto, no un stream continuo (KU-4)** **[Logic/unit]** +Dado el cruce de la condición de Regla 9 (victoria o derrota), cuando se emite el evento correspondiente, entonces se emite exactamente una vez por veredicto — nunca como una señal repetida en cada frame posterior. + +**AC-K58 — UI/HUD renderiza el telegrafío legible a partir del estado expuesto** **(bloqueado — UI/HUD sin GDD)** **[Integration]** +*Mock Contract Assumptions: el mock de UI/HUD expone `subscribe_intent_cycle(callback)` y registra cada evento discreto recibido, cuyo payload incluye **todos** los campos que las tablas de Interactions/Dependencies y KU-1/KU-2 prometen a UI/HUD: estado del ciclo, flag `ENRAGED`, progreso continuo de telegrafío `[0.0,1.0]`, identidad de `marked_target`/`marked_threat`, y — por héroe bajo telegrafío/ataque — `time_to_death` e `incoming_damage_rate_total`. Verificable hoy solo contra ese mock — la legibilidad real (composición, contraste, posición en pantalla) requiere el GDD de UI/HUD y evidencia Visual/Feel (screenshot + sign-off), fuera del alcance de este AC.* +Dado el estado expuesto por AC-K55/AC-K67, cuando un consumidor de UI/HUD se suscribe, entonces recibe cada transición discreta del ciclo de intención **y** los campos `time_to_death`/`incoming_damage_rate_total`/`ENRAGED` en el payload, sin pérdida ni duplicación de eventos — pasar este AC prueba que el contrato documentado hacia UI/HUD (no solo el ciclo de intención) se cumple. + +**AC-K59 — Audio dispara cues de advertencia en la entrada a `TELEGRAPHING` y en cada tercio** **(bloqueado — Audio sin GDD/bible)** **[Integration]** +*Mock Contract Assumptions: el mock de Audio expone `on_intent_transition(from_state, to_state)` **y** `on_telegraph_progress(tercio_index)` (o `on_telegraph_progress(progress: float)` del que el mock deriva el tercio) — porque KA-1 exige escalación sincronizada a los **tres tercios internos** de `TELEGRAPHING`, un sub-estado que un hook `from_state/to_state` no puede expresar. El sistema invoca `on_intent_transition` en cada transición del ciclo (especialmente `SEEKING→TELEGRAPHING` y la entrada a `ENRAGED` vía un hook `on_enrage()`), y `on_telegraph_progress` al cruzar cada frontera de tercio. Re-verificar contra el GDD/bible real de Audio cuando exista.* +Dado una transición `SEEKING→TELEGRAPHING` seguida del avance de tercios, cuando ocurren, entonces el mock de Audio recibe `on_intent_transition` con los estados correctos en el frame de la transición **y** `on_telegraph_progress` en cada frontera de tercio (1→2, 2→3) — cubriendo lo que KA-1 realmente requiere, no solo el cambio de estado. + +**AC-K60 — Transición de Era consume el veredicto y gestiona la consecuencia** **(bloqueado — Transición de Era sin GDD)** **[Integration]** +*Mock Contract Assumptions: el mock de Transición de Era expone `on_era_verdict(result: Victory|Defeat)`; el sistema lo invoca exactamente una vez por veredicto (mismo evento de AC-K57). Este sistema NO posee ni testea la consecuencia (props de legado, avance narrativo) — solo que el hand-off ocurre con el resultado correcto.* +Dado el veredicto declarado (AC-K49/AC-K51), cuando se emite el evento de KU-4, entonces el mock de Transición de Era lo recibe con el resultado (`Victory`/`Defeat`) correcto en el mismo frame de la declaración. + +### J. Gate de ataque, Furia, honestidad de daño y concurrencia (Reglas 4/6b/8b/10) + +**AC-K61 — Gate de ataque: el kaiju no aplica daño durante `SEEKING`/`TELEGRAPHING`** **[Integration]** +Dado un kaiju en `SEEKING` o `TELEGRAPHING` con el objetivo dentro de `attack_range` y su cooldown de ataque listo, cuando el FSM de enganche de Combate/Daño se evalúa, entonces **no** se dispara `apply_damage` — el único `apply_damage` del kaiju en todo el ciclo proviene de la resolución de `STRIKING` (Regla 4). Verifica que el kaiju nunca hace un ataque "normal" antes o durante su propio telegrafío. + +**AC-K62 — Furia por tiempo: `enrage_time_threshold_s`** **[Logic/unit]** +Dado `enrage_time_threshold_s=150.0` y héroes que han estado dañando al kaiju (disparador (b) nunca se cumple), cuando `climax_elapsed_s` cruza `150.0`, entonces `enraged` pasa a `true` en ese instante y no antes. + +**AC-K63 — Furia por desenganche (turtle total): ningún héroe comprometido** **[Logic/unit]** +Dado `hero_disengage_window_s=15.0`, `hero_engagement_fraction=0.5`, 4 héroes vivos y ninguno infligiendo daño al kaiju **ni a esbirros** desde `entered_climax` (todos parqueados fuera de peligro), cuando `climax_elapsed_s` alcanza `15.0`, entonces `engaged_hero_count=0 < required_engaged_heroes=floor(0.5×4)+1=3` y `enraged` pasa a `true` — cierra el exploit de ganar solo con tropas. + +**AC-K63b — Furia consciente del roster: un solo héroe-cebo NO evita la Furia** **[Logic/unit]** +Dado `hero_disengage_window_s=15.0`, `hero_engagement_fraction=0.5`, 4 héroes vivos, con **exactamente un** héroe comprometido (a kaiju o esbirros) cada ventana y los otros 3 retirados de todo peligro, cuando se evalúa el disparador (b), entonces `engaged_hero_count=1 < required_engaged_heroes=floor(0.5×4)+1=3` y `enraged` pasa a `true` — mantener un único héroe-cebo permanente **no** evita la Furia (cierra el exploit que un disparador "algún héroe" dejaba abierto). + +**AC-K63c — Furia consciente del roster: la fracción requerida degrada con las bajas** **[Logic/unit]** +Dado `hero_engagement_fraction=0.5` con **1** héroe vivo (los demás muertos), cuando ese único héroe está comprometido dentro de la ventana, entonces `required_engaged_heroes=clamp(floor(0.5×1)+1,1,1)=1`, `engaged_hero_count=1`, y el disparador (b) **no** se cumple — no se castiga al jugador por no poder rotar héroes que ya no tiene (la condición degenera correctamente a "ese héroe debe estar comprometido"). + +**AC-K63d — Retirada táctica corta (< ventana) NO dispara Furia** **[Logic/unit]** +Dado `hero_disengage_window_s=15.0`, `hero_engagement_fraction=0.5`, 4 héroes vivos con los 4 comprometidos, cuando el jugador retira **uno** de esos héroes por 10s (< 15s) y lo re-compromete, entonces la ventana deslizante sigue contando su daño reciente (< 15s), `engaged_hero_count` permanece en 4 ≥ `required_engaged_heroes=floor(0.5×4)+1=3` en todo instante, y `enraged` permanece `false` — una micro-retirada táctica no dispara el anti-turtle. + +**AC-K63e — Exención de esbirros: un héroe que limpia una oleada cuenta como comprometido** **[Logic/unit]** +Dado `hero_disengage_window_s=15.0`, `hero_engagement_fraction=0.5`, 4 héroes vivos, con 3 héroes que en la ventana **solo** infligieron daño a esbirros (ninguno al kaiju) y el 4º parqueado sin dañar a nada, cuando se evalúa el disparador (b), entonces `engaged_hero_count=3 ≥ required_engaged_heroes=3` (los 3 que limpiaron esbirros cuentan como comprometidos) y `enraged` permanece `false` — limpiar esbirros es estar en la pelea, no un desenganche (Regla 10b, exención de esbirros). + +**AC-K64 — Furia latcheada: monótona, nunca revierte** **[Logic/unit]** +Dado un kaiju ya `enraged` (por cualquier disparador), cuando un héroe vuelve a engancharse y a dañar al kaiju, o cuando transcurre cualquier cantidad de tiempo, entonces `enraged` permanece `true` durante todo el resto de ese `CLIMAX` — nunca vuelve a `false`. + +**AC-K65 — Furia escala telegrafío y daño (F5 valores efectivos)** **[Logic/unit]** +Dado `telegraph_duration_s=14.0`, `kaiju_attack_damage=45`, `enrage_telegraph_factor=0.6`, `enrage_damage_factor=1.5`, cuando `enraged=true`, entonces `telegraph_duration_s_effective=8.4` y `attack_damage_effective=round(45×1.5)=68` exactamente. + +**AC-K66 — En Furia, la garantía de ≥30s se suspende por diseño (no es un bug)** **[Logic/unit]** +Dado el kaiju `enraged` con los valores efectivos de AC-K65 contra un Guardián a HP completo (200), cuando se calcula `time_to_death`, entonces `kill_time_hits=ceil(200/68)=3` y `time_to_death=8.4+3×4.5=21.9s < 30s` — comportamiento esperado y documentado (la garantía AC-H29 aplica solo pre-Furia, Regla 10), no un incumplimiento del contrato con Héroes. + +**AC-K67 — `incoming_damage_rate_total` suma todas las fuentes (F6)** **[Logic/unit]** +Dado un héroe telegrafiado por el kaiju (proyectando `45/4.5=10` DPS) y simultáneamente enganchado por dos `melee_infantry` (8 DPS c/u), cuando se lee `incoming_damage_rate_total` para ese héroe, entonces el valor es `10+8+8=26` DPS — la suma de kaiju proyectado más cada esbirro enganchado, no solo el kaiju. + +**AC-K68 — Contrato de honestidad con Héroes: el readout refleja la fuente más peligrosa** **[Integration]** +Dado `time_to_death=32.0s` (kaiju-only) pero `incoming_damage_rate_total` tal que `hero_HP_current / incoming_damage_rate_total < 32.0s` (daño de esbirros acelera la muerte real), cuando Héroes (ya Approved) consume ambas señales para U-3, entonces el readout refleja la proyección más peligrosa — nunca presenta `time_to_death` como vida garantizada cuando el daño combinado la contradice (cierra el hueco de honestidad del Pilar 4, Regla 6b). *(Verificable contra el lado receptor de Héroes sin mock del propio Kaiju.)* + +**AC-K69 — Cap de concurrencia: oleadas debidas se difieren, no se descartan (Regla 8b)** **[Logic/unit]** +Dado `max_concurrent_esbirros=6`, `esbirro_wave_size=3`, con 6 esbirros vivos y una oleada debida por F3, cuando el spawner procesa el tick, entonces instancia **cero** unidades nuevas (el cap está lleno); dado que luego mueren 3 esbirros (3 vivos), cuando el siguiente tick procesa la oleada aún pendiente, entonces instancia exactamente 3 — la oleada se difirió, no se perdió, y `wave_count(t)` nunca se vio afectado. + +**AC-K70 — `climax_elapsed_s` y `wave_count(t)` son framerate-independientes** **[Logic/unit]** +Dado dos simulaciones que acumulan el mismo tiempo de juego total mediante pasos de tamaño distinto (p. ej. 60×1/60s vs 30×1/30s), cuando se comparan `climax_elapsed_s`, `wave_count(t)` y el estado de Furia resultantes, entonces son idénticos entre ambas — el pacing no depende del framerate (mismo patrón que Temporizador AC-T21; `climax_elapsed_s` acumula delta en `_physics_process`). + +**AC-K71 — RNG de `random_variation` inyectable/seedeable: desempate exacto repetible (same-plataforma)** **[Logic/unit]** +Dado una instancia de `RandomNumberGenerator` inyectada con una seed fija, cuando se ejecuta dos veces el mismo escenario de selección de objetivo **en el mismo build/arquitectura** (incluyendo el escenario de empate exacto de AC-K48), entonces ambas ejecuciones producen idéntica selección — la aleatoriedad es determinista bajo seed fija, sin `randf`/`randi` globales (coherente con coding-standards "no random seeds"). **Alcance (godot-specialist)**: el AC se acota a determinismo **same-plataforma** (la salida flotante de `randf_range`/`randi_range` no está garantizada bit-idéntica entre arquitecturas de export distintas); el test se corre en la plataforma de export objetivo, no se asume identidad byte-a-byte cross-arquitectura. El desempate final por `entity_id` (AC-K48) es determinista cross-plataforma por construcción (comparación de enteros estables), independiente de la RNG. + +**AC-K72 — F4 boundary: en el borde del rango seguro, una tropa sola SÍ puede neutralizar (comportamiento documentado)** **[Logic/unit]** +Dado `neutralize_hits_equivalent=2` (`threshold=90`) y una única `melee_infantry` (8 DPS) atacando durante todo el telegrafío, cuando se calcula el tiempo requerido, entonces `90/8=11.25s`, por debajo de cualquier `telegraph_duration_s` válido (piso 12.5s) — confirmando que la propiedad "una tropa sola no neutraliza" **no** se sostiene en todo el rango seguro y que el par `neutralize_hits_equivalent × telegraph_duration_s` debe elegirse junto (F4). + +**AC-K73 — Validación de autoría de era: la garantía de ≥30s exige `raw_TTK ≥ 30 − telegraph_duration_s`** **[Logic/unit]** +Dado el kaiju MVP (`attack_damage=45`, `attack_cooldown_s=4.5`), `telegraph_duration_s=14.0`, y un héroe de `hero_max_health=120` (dentro del rango válido `[120,200]` de Héroes), cuando se calcula F2, entonces `kill_time_hits=ceil(120/45)=3`, `raw_TTK=13.5s`, y `time_to_death=27.5s < 30s` — demostrando que un roster con héroes de <136 HP rompe AC-H29 contra el kaiju MVP, y justificando el check de autoría de era (F2, restricción de autoría). + +**AC-K74 — Check cross-GDD de la garantía de ≥30s: loud-fail al cargar `EraDefinition` con un héroe que la rompe** **[Logic/unit]** +Dado una `EraDefinition` con el kaiju MVP (`45`/`4.5`), `telegraph_duration_s=14.0`, y un roster que incluye un héroe de `hero_max_health=120` (individualmente válido en ambos GDDs), cuando se carga la era, entonces la carga **falla ruidosamente** porque `ceil(120/45)×4.5 + 14.0 = 27.5 < 30` viola la restricción (F2/OQ-11); y dado un roster donde todos los héroes cumplen `ceil(hero_max_health/45)×4.5 + 14.0 ≥ 30` (p. ej. mínimos 136 HP), cuando se carga, entonces la carga se completa sin error. El check se ejecuta como validación de datos post-carga, no como guía manual. + +**AC-K75 — Override de `STRIKING`: el golpe cae al completar el telegrafío, no un cooldown después** **[Integration]** +Dado un kaiju que completa `telegraph_duration_s=14.0` sin interrupción (cooldown suprimido durante `SEEKING`/`TELEGRAPHING`, Regla 4), cuando entra a `STRIKING`, entonces `apply_damage` se invoca en **ese mismo frame** (saltándose el piso de adquisición/espera de cooldown del FSM base para esa llamada) — el impacto NO se retrasa un `attack_cooldown_s` (4.5s) adicional; el `attack_cooldown_s` del kaiju empieza a contar **después** de este golpe. Verifica que F2 (`time_to_death` asume impacto al final del telegrafío) y AC-K10 se sostienen, y que el override de Regla 4 se aplica solo al golpe de `STRIKING`, no a esbirros ni a golpes rutinarios. + +**AC-K76 — Framerate-independencia de la ventana de amenaza (F1) y el contador de neutralización (Regla 7)** **[Logic/unit]** +Dado dos simulaciones que acumulan el mismo tiempo de juego total mediante pasos de tamaño distinto (60×1/60s vs 30×1/30s), cuando se comparan (a) el `recent_damage_dealt` de un candidato bajo la ventana deslizante de `threat_window_s` (F1) y (b) el daño acumulado hacia `threat_neutralize_damage_threshold` de un `TELEGRAPHING` en curso (Regla 7), entonces ambos acumuladores producen valores idénticos entre las dos simulaciones — ambos relojes acumulan delta en `_physics_process` (contrato de determinismo del preámbulo de Formulas), no solo `climax_elapsed_s`/`wave_count`/`enraged` (AC-K70). + +**AC-K77 — Timing de las fronteras de tercio del telegrafío (KV-1), incl. compresión en Furia** **[Logic/unit]** +Dado `telegraph_duration_s=14.0`, cuando el telegrafío avanza, entonces la frontera tercio 1→2 cae en `progress=1/3` (≈4.67s) y la 2→3 en `progress=2/3` (≈9.33s) del progreso continuo `[0.0,1.0]` expuesto (KU-1); y dado `enraged` con `telegraph_duration_s_effective=8.4`, entonces las fronteras caen en `≈2.8s` y `≈5.6s` — los tres tercios se mantienen, comprimidos proporcionalmente. Verifica la **matemática de timing** de KV-1/KV-9 (independiente de la fidelidad visual/audio, que sigue siendo evidencia Visual/Feel advisory) — el "medio timing" de la promesa de Pilar 4 queda blocking-gated. + +**AC-K78 — F6: `time_to_death` ausente (kaiju no enganchado) no se trata como "a salvo"** **[Logic/unit]** +Dado un héroe recibiendo daño solo de esbirros (el kaiju telegrafía a otro objetivo, `kaiju_projected_dps(hero)=0`), cuando se leen las señales, entonces `incoming_damage_rate_total = Σ esbirro_dps > 0` y `time_to_death` se expone como **"no aplica / sin telegrafío del kaiju"** (no un centinela `0` ni `∞`) — de modo que el consumidor use `hero_HP_current / incoming_damage_rate_total` y nunca interprete la ausencia de `time_to_death` como vida infinita. + +### K. Loud-fails de config faltantes, acoplamientos enforced y dueño único del golpe (3er re-review) + +**AC-K79 — F5 loud-fail: `enrage_time_threshold_s ≤ 0`** **[Logic/unit]** +Dado un knob `enrage_time_threshold_s=0` o negativo, cuando el sistema carga la configuración, entonces falla ruidosamente vía `push_error()` + halt (no `assert()` solo, no clamp) — un umbral ≤0 dispararía la Furia en `t=0` o la invertiría. + +**AC-K80 — F5 loud-fail: `hero_disengage_window_s ≤ 0`** **[Logic/unit]** +Dado `hero_disengage_window_s=0` o negativo, cuando el sistema carga la configuración, entonces falla ruidosamente (ventana de compromiso degenerada — el disparador (b) contaría siempre 0 héroes comprometidos). + +**AC-K81 — F5 loud-fail: `hero_engagement_fraction` fuera de `[0.34, 1.0]`** **[Logic/unit]** +Dado `hero_engagement_fraction < 0.34` (p. ej. 0.1) o `> 1.0`, cuando el sistema carga la configuración, entonces falla ruidosamente — el dominio se estrechó de `(0,1]` a `[0.34,1.0]` porque valores <0.34 reabren la degeneración de héroe-cebo en rosters grandes (`f=0.1`, `N=9 → floor(0.9)+1=1`). + +**AC-K82 — F5 loud-fail: `enrage_telegraph_factor` fuera de `(0, 1]`** **[Logic/unit]** +Dado `enrage_telegraph_factor ≤ 0` o `> 1.0`, cuando el sistema carga la configuración, entonces falla ruidosamente — un factor >1 alargaría el telegrafío en Furia (invierte el sentido), ≤0 lo anularía. + +**AC-K83 — F5 loud-fail: `enrage_damage_factor < 1`** **[Logic/unit]** +Dado `enrage_damage_factor < 1.0`, cuando el sistema carga la configuración, entonces falla ruidosamente — un factor <1 haría la Furia *menos* letal, invirtiendo su propósito. + +**AC-K84 — Regla 8b loud-fail: `max_concurrent_esbirros < 0`** **[Logic/unit]** +Dado `max_concurrent_esbirros` negativo, cuando el sistema carga la configuración, entonces falla ruidosamente — un tope negativo haría el gate de concurrencia (F3) matemáticamente degenerado (`max(0, negativo − alive)` nunca instanciaría). `0` es válido (era sin esbirros, redundante con `max_waves=0`). + +**AC-K85 — Acoplamiento F4×F2 enforced: loud-fail si una tropa sola puede neutralizar (sin flag de excepción)** **[Logic/unit]** +Dado una `EraDefinition` sin `allow_single_troop_neutralize=true`, con `kaiju_attack_damage=45`, `neutralize_hits_equivalent=2` (`threshold=90`), `telegraph_duration_s=14.0`, y un `unit_roster` cuya tropa más letal hace 8 DPS, cuando se carga la era, entonces la carga **falla ruidosamente** porque `45×2 = 90 ≤ 14.0×8 = 112` viola `kaiju_attack_damage × neutralize_hits_equivalent > telegraph_duration_s × max_single_troop_dps` (una tropa sola neutralizaría en `90/8=11.25s < 12.5s`); y dado la misma era **con** `allow_single_troop_neutralize=true`, cuando se carga, entonces la carga se completa sin error (excepción consciente). El check usa la tropa **más letal** del roster (no la menos letal) y es computable al cargar (mismo patrón cross-resource que AC-K74). + +**AC-K86 — Furia (b) no degenera en `N=2` con el default (regresión de `ceil`→`floor(f×N)+1`)** **[Logic/unit]** +Dado `hero_engagement_fraction=0.5`, `hero_disengage_window_s=15.0`, **exactamente 2 héroes vivos**, con un solo héroe-cebo comprometido y el otro parqueado sin dañar a nada, cuando se evalúa el disparador (b), entonces `required_engaged_heroes = clamp(floor(0.5×2)+1, 1, 2) = 2`, `engaged_hero_count=1 < 2`, y `enraged` pasa a `true` — con la fórmula previa `ceil(0.5×2)=1` el cebo único **evadía** la Furia (regresión cerrada). Verifica que la degeneración N=2 no vuelve con el knob en su default. + +**AC-K87 — Dueño único de `apply_damage` del kaiju: sin doble golpe en el frame de `STRIKING`** **[Integration]** +Dado un kaiju que completa `TELEGRAPHING` y entra a `STRIKING` en un frame donde su `attack_cooldown_s` está en 0 y el objetivo está en `attack_range` (la condición con la que el bucle de auto-ataque del FSM base también dispararía), cuando se procesa el frame, entonces `apply_damage` sobre el kaiju se invoca **exactamente una vez** (la del override de `STRIKING`) — el kaiju está excluido del bucle genérico de auto-ataque de Combate/Daño, así que el FSM base **nunca** dispara un segundo `apply_damage`. Verifica que el controlador de intención es el único llamador de `apply_damage` para el kaiju en todos los estados (Regla 4, contrato de dueño único). + +**AC-K88 — Abort de `STRIKING` tras cruzar 0: la victoria gana sin avanzar el ciclo** **[Integration]** +Dado el golpe de `STRIKING` que en su `apply_damage` (u otro daño del mismo frame) cruza 0 la vida del kaiju, cuando el handler de `STRIKING` continúa, entonces comprueba el cruce de 0 **antes** de avanzar a `SEEKING` o re-seleccionar objetivo y aborta de inmediato (victoria, Regla 9) — el ciclo de intención no ejecuta ni un paso más. La comprobación es síncrona en el handler, no depende del orden de fan-out de la señal de muerte (cierra la race de AC-K50). + +**AC-K89 — El kaiju no ataca ni telegrafía durante `game_time_paused` (contrato con Permadeath, cierra OQ-P1/AC-P14)** **[Integration]** +Dado un kaiju en cualquier estado del ciclo de intención (`SEEKING`/`TELEGRAPHING`/`STRIKING`/`STAGGERED`), cuando el régimen de `TimeControl` es `PAUSED` (`game_time_paused=true`, porque Permadeath lo solicitó al entrar un beat `DEATH_HOLD`), entonces el ciclo de intención no avanza — ningún reloj interno (`telegraph_duration_s`, `stagger_duration_s`, ventana de amenaza F1, contador de neutralización Regla 7) progresa, y `STRIKING` no invoca `apply_damage` mientras el flag esté activo. Al volver `game_time_paused=false`, el ciclo reanuda exactamente donde quedó, sin perder ni duplicar progreso. Testeable inyectando `TimeControl` (o un `FakeClock`) con `game_time_paused=true`, sin depender de la implementación real de Permadeath. *(Fuente del flag = TimeControl, ADR-0002 Accepted.)* + +### Clasificación de tipo de story (para el gate de evidencia) + +| AC # | Tipo | Evidencia requerida | Nivel de gate | +|---|---|---|---| +| AC-K01–K04 | Integration/Logic | Integration test / Unit test | Bloqueante | +| AC-K05–K13 | Logic/Integration | Unit test (`tests/unit/kaiju/`) / Integration test | Bloqueante | +| AC-K14–K26 | Logic/Integration | Unit test (`tests/unit/kaiju/`) — AC-K26 Integration test (sin mock, Héroes ya Approved) | Bloqueante | +| AC-K27–K33 | Logic | Unit test | Bloqueante | +| AC-K34–K42 | Logic/Integration | Unit test — AC-K38 Integration test | Bloqueante | +| AC-K43–K47 | Integration/Logic | Integration test (marca en el límite con Combate/Daño) | Bloqueante | +| AC-K48–K54 | Logic/Integration | Unit test / Integration test | Bloqueante | +| AC-K55–K57 | Integration/Logic | Integration test con suscriptor mock / Unit test | Bloqueante | +| AC-K58–K60 | Integration (bloqueado) | Integration test con mock de contrato (UI/HUD, Audio, Transición de Era) | Bloqueante (hoy solo vía mock) | +| AC-K61 | Integration | Integration test (gate de ataque en el límite con Combate/Daño) | Bloqueante | +| AC-K62–K66 | Logic | Unit test (`tests/unit/kaiju/`) — Furia F5/Regla 10 (incl. K63b/c/d/e, disparador (b) consciente del roster + exención de esbirros) | Bloqueante | +| AC-K67 | Logic | Unit test (F6) | Bloqueante | +| AC-K68 | Integration | Integration test (contrato de honestidad con Héroes, sin mock — Héroes ya Approved) | Bloqueante | +| AC-K69 | Logic | Unit test (cap de concurrencia, Regla 8b) | Bloqueante | +| AC-K70 | Logic | Unit test (framerate-independencia, patrón AC-T21) | Bloqueante | +| AC-K71 | Logic | Unit test (RNG inyectable/seedeable) | Bloqueante | +| AC-K72–K73 | Logic | Unit test (constraints de acoplamiento F4 / autoría de era F2) | Bloqueante | +| AC-K74 | Logic | Unit test (loud-fail cross-GDD de autoría de era, F2/OQ-11) | Bloqueante | +| AC-K75 | Integration | Integration test (override de `STRIKING` en el límite con Combate/Daño, Regla 4) | Bloqueante | +| AC-K76 | Logic | Unit test (framerate-independencia de ventana F1 + contador de neutralización Regla 7) | Bloqueante | +| AC-K77 | Logic | Unit test (timing de fronteras de tercio, KV-1/KV-9 — solo la matemática de timing) | Bloqueante | +| AC-K78 | Logic | Unit test (F6, `time_to_death` ausente no es "a salvo") | Bloqueante | +| AC-K79–K84 | Logic | Unit test (`tests/unit/kaiju/`) — loud-fail de los 6 knobs de config antes sin AC (F5 + Regla 8b), vía `push_error()`+halt | Bloqueante | +| AC-K85 | Logic | Unit test — acoplamiento F4×F2 enforced (cross-resource, con y sin flag de excepción) | Bloqueante | +| AC-K86 | Logic | Unit test — regresión N=2 de Furia (b) con `floor(f×N)+1` | Bloqueante | +| AC-K87–K88 | Integration | Integration test (`tests/integration/kaiju/`) — dueño único de `apply_damage` en el frame de `STRIKING` + abort tras cruce de 0, en el límite con Combate/Daño | Bloqueante | + +*Nota de rutas de test: la evidencia Logic/unit vive en `tests/unit/kaiju/`; la evidencia Integration vive en `tests/integration/kaiju/` (convención de testing-standards.md — ambas rutas aplican según la columna Tipo).* + +### Huecos/bloqueados marcados (no diferidos silenciosamente) + +1. **AC-K58** — depende de que `UI/HUD` reciba GDD (interacción diegética real del telegrafío, Visual/Feel evidence type sigue advisory una vez exista el GDD). +2. **AC-K59** — depende de que `Audio` reciba GDD/bible (hoy provisional, mismo estatus que Combate/Daño CA-1..3 y Héroes A-1..3). +3. **AC-K60** — depende de que `Transición de Era` reciba GDD (este sistema solo declara el veredicto; la consecuencia es propiedad ajena). +4. **No gating hoy, pero anotado para el futuro** — F4 (`threat_neutralize_damage_threshold`) está anclado a `kaiju_attack_damage=45` (MVP-fijado, **no** locked en Combate/Daño — ver F2 y OQ-10), no a `kaiju_max_health`, que ningún GDD define aún. Ningún AC de este documento depende de `kaiju_max_health`; solo la alternativa de re-escalado de largo plazo mencionada en Formulas F4 queda bloqueada hasta que Datos de Era/Civilización lo defina. +5. **`kaiju_definition` (Datos de Era/Civilización, ⚠️ In Design)** — la interfaz que este sistema consume (AC-K01/K02) ya está estable según ese GDD, por lo que estos AC son testeables hoy contra esa interfaz; no obstante, se recomienda re-verificar cuando el documento completo de Datos de Era/Civilización alcance Approved. El campo `max_concurrent_esbirros` (Regla 8b) es una **adición nueva** al bloque de esbirros de `kaiju_definition` (OQ-7). +6. **Cobertura de evidencia Visual/Feel (KV-1..10, KA-1..6)** — la **fidelidad** visual/audio del telegrafío (composición, contraste, bloom, timbre) sigue siendo evidencia Visual/Feel (screenshot + sign-off en `production/qa/evidence/`, gate ADVISORY), validada al construir el HUD del clímax (ver UX Flag). **Cambio tras el re-review (2026-08-12)**: la **matemática de timing** de los tercios (KV-1/KV-9, las fronteras 1/3–2/3 incl. la compresión en Furia) — que es aritmética Pilar-4-load-bearing, independiente de la fidelidad de render — **ahora sí está gateada como Logic** vía AC-K77, testeable contra la señal de progreso continuo del mock de UI (KU-1). Así el "medio timing" de la promesa de "nunca emboscar" es blocking, aunque el "medio render" siga advisory. **Nota**: el hueco de render existe también en el Approved `combate-dano.md` (sus CV/CA tampoco tienen AC) — patrón de proyecto; cerrarlo formalmente (¿enumerar KV/KA como evidencia Visual/Feel advisory por-story?) es una decisión de proceso de QA, no de este GDD. El límite de fotosensibilidad de KV-10 es un requisito de accesibilidad que se verifica al construir el HUD (evidencia Visual/Feel + check de tasa de destello). + +## Open Questions + +| # | Pregunta | Owner | Resolución objetivo | +|---|---|---|---| +| OQ-1 | `threat_neutralize_damage_threshold` (F4) hoy está anclado a `kaiju_attack_damage=45` (disponible ya). La alternativa de largo plazo — fracción de `kaiju_max_health` — auto-escalaría mejor entre kaijus futuros de distinta resistencia, pero está bloqueada hasta que `kaiju_definition` (Datos de Era/Civilización) defina ese campo | systems-designer | Al ampliar `kaiju_definition` con `max_health`, o al diseñar el segundo kaiju del roster (Alpha) | +| OQ-2 | El art bible (`design/art/art-bible.md`) no define un mood-state de Derrota (Sección 2 cubre Preparación/Clímax/Muerte/Memorial/Transición/Victoria, pero no Derrota) — este GDD declara el veredicto pero no lo escenifica (KV-8) | art-director | Al autorar el GDD de Transición de Era | +| OQ-3 | ~~`time_to_death` (F2) no descuenta daño simultáneo de esbirros~~ **RESUELTO (2026-08-11)**: se añadió `incoming_damage_rate_total` (Regla 6b/F6) que expone el DPS combinado de todas las fuentes; el readout de Héroes (U-3) debe reflejar la fuente más peligrosa (AC-K68). El hueco de honestidad del Pilar 4 queda cerrado a nivel de contrato de datos. Queda para `/ux-design` solo la *presentación* diegética de la señal combinada, no su existencia | ux-designer | Al diseñar el HUD del clímax (`/ux-design`) — solo presentación | +| OQ-4 | ~~**Acción de Phase 5 pendiente**: Combate/Daño, Héroes y Temporizador siguen citando "Encuentro con Kaiju sin GDD"~~ ✅ **RESUELTO 2026-08-12** (`/consistency-check`): des-flaggeados y re-verificados contra el contrato real — Combate/Daño (AC-C32–C34 desbloqueados, AC-C35 parcial, OQ-5/7/8 cerrados), Temporizador (AC-T03b desbloqueado, AC-T07b parcial, OQ-2/4/5 cerrados), Héroes (AC-H29/H30/H32b contrato de datos resuelto), Tropas (AC-25 desbloqueado). Pendiente menor: OQ-10 (lock de stats del kaiju en `combate-dano.md`) sigue abierto | game-designer | ✅ Cerrado | +| OQ-5 | El art bible no confirma formalmente que Sangre Vieja escalando en valor/bloom/pulso sea el vocabulario oficial de "amenaza del kaiju en curso" (KV-4) — hoy es una extensión razonada de sus principios 4.2/4.5, no una entrada explícita del documento | art-director | Próxima revisión del art bible | +| OQ-6 | `kaiju_definition` (Datos de Era/Civilización) sigue **In Design** — la interfaz que este sistema consume (`attack_damage`/`attack_range`/`attack_cooldown_s`/`aggro_range`, más los datos de esbirros) ya está estable, pero AC-K01/AC-K02 deberían re-verificarse formalmente cuando ese documento alcance Approved | game-designer | Cuando Datos de Era/Civilización se apruebe | +| OQ-7 | El schema exacto de "datos de esbirros" dentro de `kaiju_definition` (roster de tipos, cuáles `CombatProfile` por tipo, cómo se referencia `max_waves`/`esbirro_wave_size` por era) no está definido campo por campo — este GDD asume que existen sin fijar su forma exacta | systems-designer | Al ampliar `kaiju_definition` con el bloque de esbirros | +| OQ-8 | Los pesos de F1 (`threat_damage_weight=1.0`, `hero_threat_bonus=100`, `threat_random_range=15`) no tienen validación de playtest — podrían sentirse demasiado deterministas (el kaiju "siempre" va por el héroe) o demasiado erráticos según cómo se combinen en partidas reales | systems-designer | Playtest del MVP | +| OQ-9 | Este documento diseña un único kaiju (MVP, 1 civilización). El roster completo de la visión (4-5 kaijus únicos, Alpha) necesitará que `threat_neutralize_damage_threshold`, `telegraph_duration_s`, los knobs de Furia (`enrage_*`) y la cadencia de esbirros se re-tuneen por kaiju — fuera de alcance aquí, pero el patrón de fórmulas (F1-F6) debería sostenerse sin cambios estructurales | game-designer | Al diseñar el segundo kaiju (Alpha) | +| OQ-10 | **Stats "MVP-fijados" no locked**: `kaiju_attack_damage=45` y `attack_cooldown_s=4.5` son knobs tunables en Combate/Daño (no locked), pero la garantía de ≥30s de Héroes AC-H29 depende de ellos vía F2. Falta un mecanismo formal (lock explícito en Combate/Daño, o un trigger de re-verificación cross-GDD) que impida que un re-tuning de esos knobs rompa AC-H29 en silencio. Acción sugerida: al tocar Combate/Daño de nuevo, marcar esos dos campos con una nota "cambiar aquí obliga a re-verificar Encuentro con Kaiju F2 / Héroes AC-H29" | systems-designer / game-designer | Próxima edición de Combate/Daño o `/consistency-check` | +| OQ-11 | ~~**Validación de autoría de era para la garantía de ≥30s**: ¿check de datos automático o guía manual?~~ **RESUELTO (2026-08-12)**: se decidió **loud-fail obligatorio al cargar `EraDefinition`** — la restricción `ceil(hero_max_health / kaiju_attack_damage) × kaiju_attack_cooldown_s + telegraph_duration_s ≥ 30` (F2) es computable con datos disponibles al cargar la era, así que se enforce, no se documenta (ver Edge Cases "Config inválido cross-GDD", F2 restricción de autoría, AC-K74). Implementar como `validate()` post-`ResourceLoader.load()` (cruza campos de kaiju + roster de héroes) | systems-designer / game-designer | ✅ Cerrado (implementar en el schema de `EraDefinition`) | +| OQ-12 | **Cota dura de duración total del clímax — diferida conscientemente al post-MVP (aceptada 2026-08-12)**: con `enrage_damage_factor`/`enrage_telegraph_factor` fijos, un jugador con DPS alto y neutralización perfecta podría teóricamente sobrevivir indefinidamente aun `ENRAGED` — la Furia acota *cuándo empieza* la escalada, no la duración total. **Decisión MVP: se acepta a sabiendas como riesgo documentado** (no bug); la palanca de cierre duro es un **ramp progresivo de daño post-Furia** (sube por telegrafío resuelto hasta one-shotear), fuera de alcance del MVP. Si el playtest muestra estancamientos persistentes tras la Furia, OQ-12 es la primera palanca a implementar | systems-designer / game-designer | Playtest del MVP | +| OQ-13 | **Perceptibilidad del audio del tercio 1 bajo mezcla de combate densa**: KA-1 ahora exige que el audio del tercio 1 sea perceptible "con los ojos en otra parte" como canal de aviso fiable de Pilar 4. Falta validar que realmente lo sea bajo una mezcla con tropas, esbirros y otros héroes en combate — riesgo de que se pierda en el ruido. Depende además de que exista dirección/bible de audio | audio-director / ux-designer | Playtest del MVP / al autorar el bible de audio | +| OQ-14 | **Histéresis/anti-parpadeo de la señal dual `time_to_death` vs `incoming_damage_rate_total`**: este sistema expone valores crudos por frame; cuando la fuente más peligrosa cambia (un esbirro enganchado muere), el indicador del readout puede parpadear. El suavizado/histéresis es del consumidor (readout de Héroes / HUD del clímax) — falta que `/ux-design` decida el modelo (¿debounce, banda muerta, coherente con la histéresis que Héroes OQ-6 ya contempla para su propio umbral?) | ux-designer | Al diseñar el HUD del clímax (`/ux-design`) | +| OQ-15 | **Interacción farmeo-de-muertes × degradación de Furia (b)** — ✅ **RESUELTO como tradeoff aceptado para el MVP (2026-08-21).** `required_engaged_heroes` degrada con `living_hero_count` (fairness, AC-K63c), así que sacrificar un héroe desenganchado relaja el listón anti-turtle. **Decisión: se acepta a sabiendas como tradeoff documentado, no como bug** — el sacrificio deliberado **no es gratis**: cuesta una muerte permanente real (Pilar 1) y la civilización pierde ese héroe para siempre; ese costo permanente es el contrapeso de diseño. Refuerzan la aceptación dos cotas ya existentes: (1) el disparador (a) por tiempo (`enrage_time_threshold_s=150s`) es **roster-independiente** — el turtling está acotado pase lo que pase; (2) `relic_quality` (Héroes H4) gatea la *calidad*: una muerte descuidada (D=0) topa en Delgada (0.10), solo una deliberada y en juego alto da Sólida/Legendaria — farmear reliquias *buenas* ya exige riesgo real. **Contra-mecanismo demovido a fallback condicional (NO aplicado):** el "guard" de latchear el listón ante una muerte por Sacrificio deliberado (D=1) queda como palanca de reserva, a implementar **solo si** el playtest del vertical slice muestra que el sacrificio-para-turtle es una estrategia *dominante* — no se muta F5/AC-K63c/d/e preventivamente. **Acción**: medir en playtest del vertical slice; aplicar el guard únicamente si emerge dominancia | game-designer / systems-designer | Playtest del vertical slice (solo si emerge dominancia) | diff --git a/design/gdd/forja-de-legado.md b/design/gdd/forja-de-legado.md new file mode 100644 index 0000000000..90943df128 --- /dev/null +++ b/design/gdd/forja-de-legado.md @@ -0,0 +1,349 @@ +# Forja de Legado + +> **Status**: Approved (2026-08-17) — design-review con 5 agentes: NEEDS REVISION → 2 bloqueantes + 8 recomendados aplicados → aceptado. Ver `design/gdd/reviews/forja-de-legado-review-log.md` +> **Author**: usuario + agentes +> **Last Updated**: 2026-08-17 +> **Implements Pillar**: El Pasado es Poder (Pilar 2 — **gozne de datos**); Sacrificio con Peso (Pilar 1) + +> **⚠️ Riesgo de programa (top-line, registrado en design-review 2026-08-17):** Forja es el *gozne de datos* del Pilar 2, pero **no entrega payoff jugable por sí sola** — su contribución al Pilar 2 depende al 100% de que se construyan los sistemas downstream sin GDD aún (Reliquias/Bendiciones #12 para el poder en combate, Salón Conmemorativo/Panteón #14 para la exhibición). Hasta que #12/#14 existan, la cadena del Pilar 2 termina en una estructura de datos que ningún jugador percibe. Forja es honesta sobre ser "fontanería" (ver Player Fantasy) — este aviso solo hace explícita esa dependencia como riesgo, no como defecto. + +## Overview + +Forja de Legado es el sistema que **cierra la cadena central del juego**: recibe de Permadeath el veredicto final de una muerte de héroe (`hero_id`, `hero_name`, `relic_category`, `relic_quality`) y lo transforma en una **Reliquia** — un objeto de datos permanente, con el nombre del héroe caído, que la civilización hereda. Es la mitad ejecutora del Pilar 2 ("El Pasado es Poder"): donde Permadeath sella *que* la muerte fue definitiva, Forja de Legado hace literal la promesa de que esa muerte *produce algo* — nunca nada. A nivel de datos es una capa delgada de transformación y persistencia: consume el tier ya resuelto por Héroes (Delgada/Sólida/Legendaria, vía `relic_quality`) sin recalcularlo, construye la estructura de la Reliquia (identidad, categoría, tier, y la procedencia narrativa de cómo murió el héroe), dispara el autosave obligatorio (Guardado/Persistencia) para que la reliquia sobreviva a un cierre inmediato, y la deposita en el panteón acumulado del jugador. **No** decide qué hace la reliquia en combate (eso es Reliquias/Bendiciones), ni cómo se muestra el panteón (eso es Salón Conmemorativo/Panteón) — su responsabilidad es *nacer la reliquia y volverla permanente*, no *usarla* ni *exhibirla*. A nivel de jugador, es el momento —presenciado justo después del beat de muerte— en que la tragedia se convierte en un objeto sagrado con nombre propio: la espada que otro héroe empuñará generaciones después llevará el nombre del que la forjó con su caída. Sin este sistema, Permadeath tendría un veredicto sin destino y el panteón no tendría de dónde nacer: Forja de Legado es el gozne que convierte el peso del **Pilar 1** en el poder del **Pilar 2**. + +## Player Fantasy + +*(Encuadre validado por `creative-director` en el design-review 2026-08-17: la fantasía de "testigo, no forjador" se sostiene y es coherente con el pilar; la garantía anti-nihilismo NO se considera que socave las stakes — estas viven en el *tier/calidad* de la reliquia y en el *héroe nombrado*, no en existencia-vs-nada, lo cual está lockeado a nivel de concepto ["la derrota nunca es game-over"]. Ver el Riesgo de programa top-line arriba: el payoff jugable de esta fantasía depende de #12/#14.)* + +**El jugador es quien ve su tragedia volverse permanente y suya.** Justo después del beat de muerte —cuando la silueta del héroe se agrietó en fragmentos de vitral y se resolvió en un ícono dorado (el momento que Héroes/Permadeath escenifican)— Forja de Legado es lo que hace que ese ícono **deje de ser una imagen y se vuelva un objeto real que el jugador posee**: una Reliquia con el nombre del héroe caído, que a partir de ahora existe en su panteón y que ninguna derrota futura, ningún cierre del juego, puede borrar. La fantasía no es "yo forjo" como una acción que el jugador ejecuta con un botón —no hay botón, igual que en Permadeath no hay "deshacer"— sino "yo *veo nacer* algo que llevará el nombre de quien perdí, y sé que es mío para siempre". Es el instante exacto en que la palabra del concepto se cumple: *"cada reliquia legendaria tiene el nombre de un héroe que tú viste morir"*. El dolor del Pilar 1 (lo perdí) se convierte, ante sus ojos, en el poder del Pilar 2 (lo tengo, ahora como arma para el futuro) — y esa conversión es la única fuente de brillo dorado en un mundo por lo demás apagado (art bible Principio A: "El Legado es Luz"). + +Debajo del momento presenciado hay una capa que el jugador nunca ve directamente pero de la que depende toda la honestidad de la fantasía: la transformación del veredicto en datos, la escritura a disco que vuelve la reliquia irreversible, el depósito en el panteón. Esa capa no tiene emoción propia —es fontanería— pero es la que garantiza que la promesa sea verdadera: si una reliquia pudiera perderse por un crash mal manejado, o si una muerte pudiera no producir nada, la fantasía de "el pasado es poder" sería una mentira. Forja de Legado existe para que **nunca lo sea**: toda muerte de héroe produce exactamente una reliquia, siempre, y esa reliquia sobrevive a todo lo que venga después. + +**Test de diseño (este sistema debe hacerlo cumplir):** si alguna muerte de héroe llegara a producir *cero* reliquias —por un tier degenerado, un payload incompleto, o un fallo de guardado silencioso— el diseño está roto. La válvula anti-nihilismo de Héroes (piso `relic_quality`=0.10) solo tiene sentido si Forja la honra sin excepción: una reliquia Delgada y triste sigue siendo una reliquia con nombre. + +## Detailed Design + +### Core Rules + +1. **Único punto de creación de Reliquia.** Forja de Legado es el único sistema que construye una Reliquia. Recibe de Permadeath, en la transición a `RESOLVED`, el veredicto final (`hero_id`, `hero_name`, `relic_category`, `relic_quality`) — **exactamente una vez por héroe** (garantizado por Permadeath AC-P16, hand-off idempotente). Cada invocación produce **exactamente una** Reliquia. Forja nunca crea reliquias por ninguna otra vía ni a partir de la muerte de una tropa (solo héroes generan legado, Pilar 1). + +2. **Mapeo `relic_quality → tier` (consume los cortes de Héroes, no los recalcula).** Forja deriva el tier discreto de la reliquia desde el `relic_quality` recibido, usando los mismos cortes ya fijados por Héroes y consumidos por UI: **Delgada `[0.10, 0.35)`, Sólida `[0.35, 0.70)`, Legendaria `[0.70, 1.00]`** (límites inferiores inclusivos, coherente con Héroes AC-H19/H20). Forja **nunca recomputa** `relic_quality` (eso lo posee Héroes vía H4) — solo lo mapea a una banda. Guarda **ambos**: el `relic_quality` float exacto (para consumidores futuros y desempates) y el `tier` discreto (para consumidores que solo necesitan la banda). + +3. **Estructura de la Reliquia (`RelicRecord`).** La Reliquia forjada es un registro con estos campos: + - `relic_id` — identificador estable y único, asignado por Forja (Regla 4) + - `source_hero_id`, `source_hero_name` — identidad del héroe caído (transportados) + - `relic_category` — `offensive` / `defensive` / `rally` (transportada, nunca reasignada) + - `relic_quality` — float `[0.10, 1.00]` (transportado exacto) + - `tier` — `Delgada` / `Sólida` / `Legendaria` (derivado, Regla 2) + - `origin_era_id` — de qué era/civilización provino (Regla 6) + - `forge_index` — orden de forja dentro del legado (contador monótono, Regla 4) + - `epitaph` — descriptor de procedencia derivado de `relic_category × tier` (p. ej. plantilla "Guardián caído — reliquia sólida"); es *narrativa de procedencia*, no efecto mecánico + + **Modelo de "historia" de la reliquia (aclaración de diseño, design-review 2026-08-17).** El concepto promete que cada reliquia "refleja cómo murió" el héroe. En el schema actual, la **identidad narrativa única** de cada reliquia la portan `source_hero_name` + `origin_era_id` + `relic_category` (los tres, únicos/estables por héroe caído): *quién* cayó, *en qué era*, y *qué tipo de legado* dejó (la categoría **es** la codificación de "cómo murió" — fija por héroe, Héroes AC-H06). El `epitaph` es, **por diseño, una etiqueta de procedencia terse** (1 de 9 plantillas `categoría×tier`), NO una narrativa por-muerte: dos reliquias `offensive×Legendaria` comparten el mismo texto de `epitaph` pero **no** la misma identidad (distinto `source_hero_name`/`origin_era_id`). Forja no incluye un token de contexto-de-muerte (D/T/P) en la `RelicRecord` — esa evidencia se congela y consume aguas arriba (Héroes/Permadeath). Si el Panteón #14 llegara a necesitar variación de texto por-muerte más fina que `categoría×tier`, es una ampliación de schema a coordinar cuando #14 se diseñe (ver OQ-FL3), no una deuda oculta de este GDD. + +4. **`relic_id` y `forge_index`: estables, únicos, nunca reutilizados.** Forja mantiene un contador de forja **monótono persistido** en el guardado; `forge_index` es su valor en el momento de forjar (0, 1, 2, … a lo largo de todo el legado, nunca reiniciado entre eras). `relic_id` se deriva de forma determinista y única a partir de ese contador (nunca del `get_instance_id()` de Godot — mismo criterio anti-reciclaje que Kaiju `entity_id`: debe ser reproducible y estable a través de guardados/cargas). Este `relic_id` es el mismo que Guardado persiste ("qué IDs de reliquia se forjaron", Guardado Regla 4) y el que `EraDefinition.legacy_props` referencia (Datos de Era Regla 4) — es la **fuente de verdad canónica** del ID de una reliquia. + + **Tipo y alcance de unicidad de `relic_id` (fijado en design-review 2026-08-17; el esquema de *encoding* sigue en OQ-FL5).** `relic_id` es de tipo **`String`** (no `int`), **único a través de todo el legado** (global al save, no solo por era), y estable a través de guardados/cargas. El GDD fija estas tres propiedades del contrato (tipo, alcance global, estabilidad); el *formato exacto* de derivación desde `forge_index` (¿`"relic_%d" % forge_index`? ¿`"{origin_era_id}:{forge_index}"`? ¿un hash estable?) es de implementación y se decide en el ADR de OQ-FL5. Se elige `String` sobre `int` crudo para que el encoding pueda incorporar la era u otro prefijo sin cambiar el tipo del campo que Guardado/Datos de Era ya referencian. + +5. **Anti-nihilismo: toda muerte produce exactamente una reliquia, nunca cero** (Pilar 2, honra la válvula de Héroes). Incluso una muerte desperdiciada (`relic_quality = 0.10`, Tier Delgada) produce una reliquia real con el nombre del héroe. Forja nunca descarta, funde ni "salta" una muerte por tener tier bajo — una reliquia Delgada y triste sigue siendo una reliquia. Un `relic_quality` fuera de `[0.10, 1.00]` es un contrato roto de Héroes y **falla ruidosamente** en build de desarrollo, nunca se clampa en silencio para producir cero. + + **Reconciliación del piso 0.10 con Héroes (design-review 2026-08-17).** El piso `0.10` es responsabilidad de Héroes (`base_accidental`, válvula anti-nihilismo de H4), pero su tuning knob permitía antes un rango `0.05–0.15` — un `base_accidental = 0.05` legítimo habría producido `relic_quality = 0.05` en cada muerte accidental (`D=0`), **fuera** del dominio `[0.10, 1.00]` que Forja asume, disparando el loud-fail de Forja en cada muerte desperdiciada. Se corrigió aguas arriba: el rango seguro de `base_accidental` en `sistema-de-heroes.md` se **estrechó a `0.10–0.15`** (coherente con el "Rango de salida [0.10,1.00]" que el propio Héroes ya declaraba), de modo que H4 nunca puede emitir por debajo del piso de dominio de Forja. El loud-fail de Forja queda como red de seguridad ante un contrato roto, no como ruta esperable. + +6. **Depósito en el registro + autosave obligatorio (orden fijo).** Tras construir la `RelicRecord`, Forja: (a) la **añade al registro de reliquias del jugador** — Forja es la única fuente de verdad de qué reliquias existen (coherente con Datos de Era Regla 4 y Guardado Regla 4); (b) **dispara el autosave obligatorio** (Guardado AC-02) para persistirla. El orden es estricto: la reliquia se añade al registro **antes** de disparar el autosave, para que la escritura capture la reliquia recién forjada. El juego no continúa hasta que el autosave resuelve (Guardado Regla 1 / AC-04) — la reliquia es irreversible en cuanto el `SAVING` completa. + +7. **Forja no posee el efecto de combate ni la exhibición.** Forja produce y persiste la `RelicRecord`; **no** define qué otorga la reliquia en combate (eso lo consume Reliquias/Bendiciones #12, leyendo `relic_category` + `tier`), ni cómo se muestra el panteón (eso lo consume Salón Conmemorativo/Panteón #14, leyendo el registro). Ambos son contratos **provisionales** — ninguno tiene GDD aún; Forja define la forma del dato que deberán respetar (igual que Permadeath definió el hand-off que Forja respeta). + +8. **Deduplicación defensiva.** Si Forja recibiera dos veces un veredicto para el mismo `source_hero_id` en la misma `origin_era_id` (bug de doble entrega, que Permadeath AC-P16/AC-P17 ya previene aguas arriba), Forja **no** forja una segunda reliquia — una muerte = una reliquia. La segunda invocación es un no-op registrado con advertencia en build de desarrollo, no un duplicado silencioso. + + **Por qué la clave `(source_hero_id, origin_era_id)` es segura — contrato de unicidad de instancia de héroe (fijado en design-review 2026-08-17).** La clave de dedup solo es correcta si `source_hero_id` identifica una **instancia de héroe única**, no un arquetipo/plantilla compartido. Contrato confirmado: **cada héroe es una unidad nombrada única dentro del roster de su era** — los héroes NO se refuerzan ni reaparecen dentro de una era (a diferencia de las tropas, que sí tienen `reinforcement_ratio`), y el estado `DEAD` es terminal (Héroes AC-H27b). Por tanto un héroe muere **a lo sumo una vez por era**, y `(source_hero_id, origin_era_id)` identifica unívocamente una muerte. La única forma de colisión legítima sería un **error de autoría de datos**: dos entradas del roster de una era con el mismo `hero_id`. Forja se protege de eso validando, al activarse una era (`LOADED`), que los `hero_id` del roster de héroes sean únicos — **loud-fail** ante un duplicado (ver AC-FL26), en vez de arriesgar que la dedup trague silenciosamente una segunda muerte legítima. *(Si en el futuro se introdujera respawn/refuerzo de héroes, la clave de dedup debería migrar a un token de instancia-de-muerte por muerte en el payload de Permadeath — cambio cross-GDD; hoy explícitamente fuera de alcance.)* + +### States and Transitions + +*(Estado por reliquia en curso de forja — no un estado global del sistema. Una reliquia se procesa de forma esencialmente síncrona; los estados marcan los puntos de compromiso para el manejo de fallos.)* + +| Estado | Descripción | Transición desde | Transición hacia | +|---|---|---|---| +| `RECEIVED` | Veredicto recibido de Permadeath en `RESOLVED`; validado (4 campos presentes, `relic_quality ∈ [0.10,1.00]`) | Hand-off de Permadeath | `FORGED` (validación OK); *loud-fail* (payload incompleto/fuera de rango, Edge Cases) | +| `FORGED` | `RelicRecord` construido: `relic_id`/`forge_index` asignados, `tier` derivado, `epitaph` compuesto | `RECEIVED` | `DEPOSITED` | +| `DEPOSITED` | Reliquia añadida al registro en memoria (fuente de verdad); autosave disparado | `FORGED` | `PERSISTED` (autosave OK); fallo bloqueante de guardado (Guardado AC-35, el juego no continúa) | +| `PERSISTED` | Autosave resuelto a `SAVE_EXISTS`; la reliquia es irreversible y visible para consumidores | `DEPOSITED` | — (terminal) | + +### Interactions with Other Systems + +| Sistema | Dirección | Interfaz | +|---|---|---| +| Permadeath | Permadeath → Forja | Recibe el veredicto final (`hero_id`, `hero_name`, `relic_category`, `relic_quality`) en `RESOLVED`, una sola vez. **Cierra Permadeath OQ-P3 / AC-P16** (Forja era el consumidor crítico sin GDD) | +| Guardado/Persistencia | Forja → Guardado | Dispara el autosave obligatorio tras forjar (Guardado AC-02); Forja posee los datos de reliquia, Guardado los escribe. El juego no continúa hasta `SAVE_EXISTS` | +| Datos de Era/Civilización | Datos → Forja (solo lectura) | Lee qué `era_id` está `ACTIVE` para etiquetar `origin_era_id` de la reliquia. `EraDefinition.legacy_props` referencia los `relic_id` que Forja asignó (Datos de Era Regla 4) | +| Reliquias/Bendiciones | Forja → #12 | Expone la `RelicRecord` (`relic_category` + `tier`) como input para determinar el efecto de combate. *Contrato provisional — sin GDD* | +| Salón Conmemorativo/Panteón | Forja → #14 | Expone el registro de reliquias para exhibición (nombre, categoría, tier, epitafio, era). *Contrato provisional — sin GDD* | +| Transición de Era | Forja → #15 | El registro de reliquias es parte del legado que persiste entre eras. *Contrato provisional — sin GDD* | +| Narrativa Ambiental | Forja → #17 (indirecta) | Consume `legacy_props` (IDs de reliquia de eras anteriores, vía Datos de Era) para poblar props; esos IDs son los `relic_id` de Forja. *Contrato provisional — sin GDD* | + +> **Nota de fuente de verdad**: Forja de Legado es el **dueño canónico** de los datos de una reliquia. Guardado persiste el registro por referencia (IDs + campos), Datos de Era lo referencia vía `legacy_props`, y el Panteón/Reliquias lo leen — pero ninguno duplica ni redefine la `RelicRecord`. Coherente con la regla de "persistencia por referencia, nunca por copia" de Guardado (Regla 4) y Datos de Era (Regla 4). + +## Formulas + +Forja de Legado es un sistema de transformación/persistencia; no tiene fórmulas de balance (el `relic_quality` ya lo resolvió Héroes vía H4, y el efecto de combate de la reliquia es de Reliquias/Bendiciones #12). Su única lógica numérica es un mapeo escalón determinista y un contador. *(Modo lean: `systems-designer` no spawneado — no hay balance ni curvas que evaluar, solo un escalón que consume cortes ya registrados y un contador monótono; mismo criterio que Permadeath P1.)* + +### Fórmula FL1 — Mapeo `relic_quality → tier` + +Función escalón que asigna la banda discreta de la reliquia. **Consume los cortes ya fijados por Héroes** (registrados en `relic_quality`) — no los redefine. + +`tier(relic_quality) = Delgada si 0.10 ≤ relic_quality < 0.35` +` = Sólida si 0.35 ≤ relic_quality < 0.70` +` = Legendaria si 0.70 ≤ relic_quality ≤ 1.00` + +| Variable | Símbolo | Tipo | Rango | Descripción | +|----------|---------|------|-------|-------------| +| Calidad de reliquia | `relic_quality` | float | `[0.10, 1.00]` | Recibido de Permadeath; computado por Héroes H4. Fuera de rango → loud-fail (Regla 5) | +| **Tier de reliquia** | `tier` | enum | {Delgada, Sólida, Legendaria} | Banda discreta consumida por #12 (efecto) y #14 (exhibición) | + +**Rango de salida**: exactamente 3 valores discretos. Límites inferiores **inclusivos**, superiores exclusivos (salvo el tope 1.00, inclusivo) — idéntico a Héroes AC-H19 (0.35 → Sólida) y AC-H20 (0.70 → Legendaria). Sin división: estructuralmente no hay divide-by-zero. + +**Comparación de boundary con tolerancia epsilon (fijado en design-review 2026-08-17).** `relic_quality` NO es un valor autoral: Héroes lo computa en H4 vía una cadena `clamp(base_accidental + D×(base_deliberate − base_accidental + w_stakes×T + w_purpose×P), …)`, donde `T` es un float continuo (`1 − elapsed_s/approach_duration_s`). Por tanto un valor que "debería" caer exacto en un boundary (0.70 es alcanzable resolviendo `0.35·T = 0.30` → `T ≈ 0.857142…`) puede computarse como `0.6999999997` o `0.7000000003` por aritmética de punto flotante. FL1 **debe** comparar los boundaries con una tolerancia `EPSILON_TIER = 1e-6`: un valor dentro de `±EPSILON_TIER` de un corte se clasifica en el tier **superior** (coherente con "límite inferior inclusivo" — `0.70 − ε` cuenta como `0.70` → Legendaria). Sin esta tolerancia, una decisión climática que el jugador "ganó" en el boundary podría degradarse al tier inferior por ruido de bits. *(El boundary `0.35` no es alcanzable por la fórmula real — ver nota de conjunto alcanzable abajo — pero la tolerancia se aplica uniformemente a ambos cortes por robustez.)* + +**Nota de conjunto alcanzable (informativa, no un defecto — design-review 2026-08-17).** Con las constantes MVP (`base_accidental=0.10`, `base_deliberate=0.40`), el conjunto de valores que H4 realmente produce es `{0.10} ∪ [0.40, 1.00]`: una muerte accidental (`D=0`) da exactamente `0.10`, y una deliberada (`D=1`) da como mínimo `0.40` (T=0, P=0). Consecuencia sobre los tiers de FL1: **Delgada `[0.10,0.35)` ⟺ muerte accidental** (solo el valor `0.10` la alcanza); la franja `[0.35, 0.40)` y el punto exacto `0.35` **nunca** se producen en juego real; **Sólida `[0.35,0.70)` es alcanzable** desde `0.40` hacia arriba, y **Legendaria** normalmente. Esto es un mapeo limpio y probablemente intencional (accidental → Delgada; deliberada → Sólida/Legendaria), **propiedad del tuning de Héroes, no de Forja** — Forja mapea cortes que no posee. Los boundary-tests de FL1 (AC-FL04) siguen siendo defensa válida aunque su valor exacto no se produzca en la tubería integrada. + +**Ejemplos** (mismos boundaries que Héroes ya testea): +1. `relic_quality = 0.10` → Delgada (piso anti-nihilismo, una muerte desperdiciada igual produce reliquia) +2. `relic_quality = 0.3499` → Delgada; `relic_quality = 0.35` → Sólida (boundary inclusivo abajo) +3. `relic_quality = 0.6999` → Sólida; `relic_quality = 0.70` → Legendaria (boundary inclusivo abajo) +4. `relic_quality = 1.00` → Legendaria (tope inclusivo) + +### Fórmula FL2 — Asignación del índice de forja + +`forge_index(nueva_reliquia) = forge_counter` (valor actual, antes de incrementar) +`forge_counter ← forge_counter + 1` (tras asignar) + +| Variable | Símbolo | Tipo | Rango | Descripción | +|----------|---------|------|-------|-------------| +| Contador de forja | `forge_counter` | int | `[0, ∞)` | Monótono, persistido en el guardado, nunca reiniciado entre eras | +| Índice de forja | `forge_index` | int | `[0, ∞)` | Valor asignado a la reliquia; único y estable por reliquia dentro del legado | + +**Rango de salida**: enteros no negativos, estrictamente crecientes. Arranca en 0 en un legado nuevo (sin guardado previo). Al cargar, continúa desde el valor persistido — nunca retrocede ni reutiliza un índice. `relic_id` se deriva de este índice de forma determinista (Regla 4), garantizando unicidad sin depender de IDs de instancia del engine. + +**Ejemplo**: legado nuevo, mueren 3 héroes en secuencia → `forge_index` = 0, 1, 2; `forge_counter` queda en 3 y se persiste. Tras recargar y forjar una 4ª → `forge_index` = 3 (continúa, no reinicia). + +## Edge Cases + +- **Si llega un payload incompleto o nulo** (falta `hero_id`, `hero_name`, `relic_category`, o `relic_quality`): **falla ruidosamente** al recibirlo en build de desarrollo — Forja no construye una reliquia con datos incompletos. Es la contraparte de Permadeath AC-P03 (que ya valida aguas arriba); Forja se protege igual, porque una reliquia es permanente y no puede nacer corrupta. Nunca sustituye un default silencioso. + +- **Si `relic_quality` llega fuera de `[0.10, 1.00]`** (contrato roto de Héroes): **loud-fail** (Regla 5) — nunca se clampa para producir un tier fuera de banda ni una reliquia "cero". El piso 0.10 es responsabilidad de Héroes (válvula anti-nihilismo); si se viola, es un bug que debe gritar, no absorberse. + +- **Si `relic_quality` cae exactamente en un boundary de tier** (`0.35` o `0.70`): FL1 resuelve con límite inferior inclusivo — `0.35` → Sólida, `0.70` → Legendaria. Idéntico a Héroes AC-H19/H20. No es ambigüedad ni bug. + +- **Si `relic_category` no es `offensive`/`defensive`/`rally`** (dato roto): **loud-fail** — Forja nunca inventa ni sustituye una categoría por defecto. La categoría es fija por héroe (Héroes AC-H06); un valor desconocido es un error de datos de Héroes/era. + +- **Si el autosave falla tras forjar** (disco lleno, permisos): Guardado agota sus reintentos y expone un fallo **bloqueante** (Guardado Regla 1 / AC-35) — el juego no continúa. La `RelicRecord` está en el registro en memoria (`DEPOSITED`) pero **no** alcanzó `PERSISTED`. Forja **no reintenta por su cuenta** — delega enteramente en Guardado. Si el jugador cierra sin que el guardado resuelva, la reliquia de esta sesión se pierde, pero el guardado previo sigue intacto (contrato de Guardado: "este evento específico no se pudo persistir, tu guardado previo está a salvo"). Coherente con el Pilar 1: una reliquia solo es irreversible una vez en disco. + +- **Si el juego crashea entre `RESOLVED` y el fin del autosave de Forja** (reliquia forjada en memoria, no persistida): al recargar, la reliquia **no** está en el registro persistido — pero el autosave de `DYING` (Héroes AC-H26) sí persistió el payload de *muerte pendiente de sellar* (incl. `relic_quality`). El flujo de carga coacciona `DYING→DEAD` y **re-forja la reliquia desde el payload persistido** (Héroes AC-H27c / Permadeath Regla 8b). La deduplicación de Forja (Regla 8) garantiza que, si la reliquia sí llegó a persistirse antes del crash, no se forje una segunda; si no, se forja ahora. Resultado: exactamente una reliquia por héroe, incluso a través de un crash a mitad de la cadena — nunca cero, nunca dos. + +- **Si Forja recibe dos veces un veredicto para el mismo `source_hero_id` en la misma `origin_era_id`** (doble entrega, que Permadeath AC-P16/AC-P17 ya previene): la segunda invocación es un **no-op** registrado con advertencia en build de desarrollo (Regla 8) — una muerte = una reliquia, nunca un duplicado silencioso con dos `relic_id` distintos. + +- **Si no hay ninguna era en estado `ACTIVE` al momento de forjar** (no debería ocurrir — una muerte de héroe ocurre siempre dentro de una era jugándose): **loud-fail** defensivo — Forja no puede etiquetar `origin_era_id` sin una era activa, y una reliquia sin era de origen rompe la referencia de `legacy_props` (Datos de Era). Nunca asigna un `origin_era_id` por defecto. + +- **Si Reliquias/Bendiciones (#12) aún no existe** (estado actual del proyecto): Forja funciona igual — acumula reliquias en el registro y las persiste; simplemente nadie consume aún su efecto de combate. Forja **no depende** de #12 para operar. Hoy verificable con un mock del consumidor en el límite de este sistema. + +- **Si el `forge_counter` creciera sin cota** (legado muy largo): es un `int` de 64 bits en Godot; el número de héroes que pueden morir en un legado es finito y ridículamente menor que ese techo. No se acota artificialmente — un límite configurable sería complejidad sin beneficio. *(Nota de serialización, design-review 2026-08-17: la afirmación "64 bits" solo sobrevive el round-trip de guardado si Guardado/Persistencia serializa con `FileAccess.store_var`/`get_var` — que preserva `int64` exacto. Si Guardado usara `JSON` (que decodifica todo número como `float`/double, 53 bits de precisión entera), la fidelidad de 64 bits NO se garantiza. Es irrelevante en la práctica —los conteos de muertes por legado son diminutos, muy por debajo de 2⁵³— pero el formato de serialización de Guardado hoy no está especificado en su GDD; a fijar en el ADR de OQ-FL4, cruzado con Guardado.)* + +## Dependencies + +**Dependencias hacia arriba (upstream) — lo que este sistema necesita:** + +| Sistema | Tipo | Interfaz | Estado | +|---|---|---|---| +| Permadeath | Dura (**productor crítico**) | Recibe el veredicto final (`hero_id`, `hero_name`, `relic_category`, `relic_quality`) en `RESOLVED`, una sola vez (Permadeath AC-P16). Es el único input que dispara una forja | ✅ In Design — **cierra Permadeath OQ-P3/AC-P16**; el contrato de hand-off ya está definido en `design/gdd/permadeath.md` (Regla 5) | +| Guardado/Persistencia | Dura | Forja dispara el autosave obligatorio tras forjar (Guardado AC-02); el juego no continúa hasta `SAVE_EXISTS`. Guardado escribe, Forja posee el dato | ✅ Designed — Guardado ya lista a Forja de Legado en sus Interactions/Dependencies (AC-02) | +| Datos de Era/Civilización | Dura (**solo lectura**) | Lee qué `era_id` está `ACTIVE` para etiquetar `origin_era_id`. La referencia inversa (`EraDefinition.legacy_props` → `relic_id`) hace la relación bidireccional | ⚠️ **Contrato provisional** — Datos de Era **no lista hoy a Forja** en su tabla de Dependientes (igual que le pasó a Permadeath con `N_max`). Requiere añadir la fila en una pasada de reconciliación — ver Open Questions OQ-FL1 | + +**Dependientes hacia abajo (downstream) — dependen de este:** + +| Sistema | Tipo | Interfaz | +|---|---|---| +| Reliquias/Bendiciones | Dura (**consumidor crítico**) | Consume `relic_category` + `tier` de cada `RelicRecord` para determinar el efecto de combate/bendición. *Sin GDD aún — Forja define el contrato provisional que #12 respetará* | +| Salón Conmemorativo/Panteón | Dura | Consume el registro de reliquias (nombre, categoría, tier, epitafio, era) para exhibirlo. *Sin GDD aún — contrato provisional* | +| Transición de Era | Dura | El registro de reliquias es parte del legado que persiste entre eras; Transición lo arrastra al cambiar de civilización. *Sin GDD aún — contrato provisional* | +| Narrativa Ambiental | Blanda (indirecta) | Consume `legacy_props` (IDs de reliquia de eras anteriores, vía Datos de Era) para poblar props; esos IDs son los `relic_id` de Forja. *Sin GDD aún — contrato provisional* | + +**Nota de consistencia bidireccional:** +- Permadeath lista a Forja de Legado como "consumidor crítico" downstream (✅ coincide con este upstream; cierra su OQ-P3). +- Guardado/Persistencia lista a Forja de Legado como consumidor que dispara autosave (✅ coincide, AC-02). +- Datos de Era/Civilización **no** lista a Forja en su tabla de Dependientes (⚠️ ver OQ-FL1) — a corregir en reconciliación. +- `systems-index.md` lista la fila de Forja con "Depends On: Permadeath" únicamente — debería añadir **Guardado/Persistencia** y **Datos de Era/Civilización** (⚠️ ver Fase 5 / OQ-FL1). + +**Relación con Reliquias/Bendiciones y Panteón (sin GDD):** Forja es el productor del cual ambos dependen. Como ninguno existe aún, Forja **define la forma del dato** (`RelicRecord`) que deberán consumir — igual que Permadeath definió el hand-off que Forja respeta. Estos contratos son provisionales hasta que esos GDDs se escriban. + +## Tuning Knobs + +Forja de Legado tiene deliberadamente **muy pocos knobs** — es un sistema de transformación/persistencia, no de balance. Los cortes de tier los posee Héroes (`relic_quality`, no se duplican aquí) y el efecto de combate de la reliquia lo posee Reliquias/Bendiciones (#12). Los únicos ajustes de este sistema son de *contenido/presentación de procedencia*, no de balance de juego. + +| Knob | Símbolo | Tipo | Default | Rango seguro | Qué se rompe en extremos | +|---|---|---|---|---|---| +| Tabla de plantillas de epitafio | `epitaph_templates` | data (dict `categoría×tier → string`) | 9 entradas (3 categorías × 3 tiers) | 9 entradas pobladas, ninguna vacía | Una entrada faltante deja una reliquia sin descriptor de procedencia legible → loud-fail al cargar la tabla en build de desarrollo (una reliquia sin epitafio rompe el Pilar 2 en la presentación). No es balance: es cobertura de contenido | + +**Knobs que NO existen (decisiones conscientes):** +- **No hay knob de cortes de tier**: los boundaries `[0.10,0.35)/[0.35,0.70)/[0.70,1.00]` los posee Héroes (`relic_quality`, registro). Forja los consume vía FL1; duplicarlos como knob crearía dos fuentes de verdad que podrían desincronizarse (mismo criterio anti-duplicación que Datos de Era/Guardado Regla 4). +- **No hay knob de efecto/poder de la reliquia**: eso es Reliquias/Bendiciones (#12). Forja no toca balance de combate — meterlo aquí contradiría el alcance delgado y pisaría #12. +- **No hay knob de tope de reliquias en el panteón**: el panteón crece sin límite por diseño (Pilar 2 — el legado se acumula). El límite de cuántas se *renderizan* a la vez es de Datos de Era (`max_legacy_props_visible`) o del Panteón (#14), no de Forja. +- **No hay knob de on/off de la forja**: toda muerte de héroe forja una reliquia (Regla 5), regla dura no configurable — desactivarla rompería el Pilar 2. + +## Visual/Audio Requirements + +*Referencia rectora: Art Bible Principio A ("El Legado es Luz"). **Forja de Legado no diseña la escenificación** — la revelación del ícono durante el beat la poseen Héroes (V-6: silueta → fragmentos de vitral → ícono de reliquia) y el timing de Permadeath (PV-5); la exhibición en el panteón la posee Salón Conmemorativo/Panteón (#14). Esta sección fija únicamente el **contrato de identidad del ícono** que Forja posee.* + +**FV-1 — Forja posee la identidad del ícono final de la reliquia, no su escenificación.** Héroes V-6 cede el ícono final a Forja ("el ícono final es propiedad de Forja de Legado; este sistema solo provee `relic_category` + `relic_quality`"). El ícono de una reliquia se determina por `relic_category` (silueta base: arma/estandarte/reliquia-defensiva) + `tier` (grado de acabado dorado). Forja garantiza que cada `RelicRecord` referencia un ícono resoluble; **no** anima la revelación ni compone la escena. + +**FV-2 — El dorado es exclusivo del legado (Principio A).** El ícono de reliquia es uno de los pocos elementos con tratamiento dorado/luminoso pleno del juego — es literalmente un objeto nacido de una muerte permanente, el caso central del test del Principio A ("¿esto existe porque un héroe murió por ello?"). El grado de brillo/acabado escala con el `tier` (Delgada = plomo dorado apenas insinuado; Legendaria = acabado dorado pleno), reforzando visualmente que una muerte bien gastada produce un objeto más radiante. *(El diseño pictórico concreto lo posee el art bible / art-director; Forja solo fija que `tier` es el input de grado de acabado.)* + +**FV-3 — Audio de la forja: handoff, sin verbo propio.** El momento sonoro del nacimiento de la reliquia (si lo hay) se sincroniza al hit-point de revelación de V-6 (Héroes A-1/Permadeath PV-4), no lo posee Forja como diseñador de sonido. Coherente con el understatement anti-melodrama del proyecto. *(Contrato provisional — sin Dirección de Audio aún.)* + +## UI Requirements + +*Forja de Legado **no tiene HUD propio** (igual que Permadeath). Esta sección fija su única frontera con la UI.* + +**FU-1 — Sin UI de forja en sesión.** Forja no expone ninguna pantalla, prompt ni readout durante el juego. La revelación de la reliquia al morir el héroe la maneja el beat de Permadeath/Héroes (V-6); la exhibición del panteón acumulado la maneja Salón Conmemorativo/Panteón (#14). Forja solo produce y persiste el dato. + +**FU-2 — Sin editorializar (understatement).** Coherente con Héroes U-5 / Permadeath PU-3: Forja no dispara ningún texto de sistema ("Reliquia forjada", "Legado creado"). El epitafio (`epitaph`) es *dato de procedencia* que consumidores (Panteón #14) pueden mostrar en su propio contexto — Forja no lo renderiza ni decide cuándo se ve. + +## Acceptance Criteria + +*Convención: formato Dado/Cuando/Entonces + etiqueta de tipo de evidencia (`[Logic/unit]`, `[Integration]`). Los AC marcados **(bloqueado)** dependen de un sistema sin GDD aún (Reliquias/Bendiciones #12, Panteón #14) — hoy verificables solo con mock en el límite de este sistema. Permadeath, Guardado/Persistencia y Datos de Era/Civilización **sí** tienen GDD — los AC contra esos contratos NO están bloqueados, solo pendientes de implementación (mismo criterio que `permadeath.md` aplicó tras su reconciliación OQ-P6).* + +### A. Recepción y creación de la Reliquia (Regla 1) + +**AC-FL01 — Un veredicto de Permadeath produce exactamente una Reliquia** **[Integration]** +Dado un veredicto (`hero_id`, `hero_name`, `relic_category`, `relic_quality`) recibido de Permadeath en `RESOLVED`, cuando Forja lo procesa, entonces construye exactamente una `RelicRecord` y la lleva a `FORGED` — ninguna invocación produce cero ni dos reliquias. + +**AC-FL02 — Solo héroes forjan reliquias; no existe punto de entrada para tropas** **[Logic/unit]** +Dado el contrato de entrada de Forja (el único hand-off que consume es el de Permadeath `RESOLVED`, propio de la muerte de un héroe, expuesto como el símbolo de entrada provisional **`receive_relic(hero_id, hero_name, relic_category, relic_quality)`** — mismo hook que Permadeath AC-P16 asume en su mock), cuando se audita la superficie pública de Forja, entonces `receive_relic` es su **único** punto de entrada de creación y no expone ningún otro método ni escucha ninguna señal que acepte la muerte de una tropa como input — la creación de reliquias está estructuralmente acotada a héroes. *(Se complementa con un check estático de CI que grepea invocaciones de `receive_relic` fuera de Permadeath — ver nota de testabilidad al final. El nombre exacto del símbolo se confirma en el ADR OQ-FL4; hasta entonces `receive_relic` es el contrato provisional que da al grep un target concreto.)* + +### B. Mapeo `relic_quality → tier` (Regla 2, Fórmula FL1) + +**AC-FL03 — Piso anti-nihilismo: 0.10 → Delgada** **[Logic/unit]** +Dado `relic_quality = 0.10`, cuando FL1 se evalúa, entonces `tier = Delgada` — el piso de Héroes produce una reliquia real, nunca ninguna. + +**AC-FL04 — Boundary inferior 0.35 (inclusivo)** **[Logic/unit]** +Dado `relic_quality = 0.3499`, cuando FL1 se evalúa, entonces `tier = Delgada`; dado `relic_quality = 0.35` exacto, entonces `tier = Sólida` (límite inferior inclusivo, coherente con Héroes AC-H19). + +**AC-FL05 — Boundary inferior 0.70 (inclusivo)** **[Logic/unit]** +Dado `relic_quality = 0.6999`, cuando FL1 se evalúa, entonces `tier = Sólida`; dado `relic_quality = 0.70` exacto, entonces `tier = Legendaria` (límite inferior inclusivo, coherente con Héroes AC-H20). + +**AC-FL06 — Tope superior 1.00 (inclusivo)** **[Logic/unit]** +Dado `relic_quality = 1.00`, cuando FL1 se evalúa, entonces `tier = Legendaria`. + +**AC-FL07 — Forja nunca recomputa `relic_quality`; almacena ambos valores exactos** **[Logic/unit]** +Dado un `relic_quality` recibido, cuando Forja construye la `RelicRecord`, entonces el campo `relic_quality` almacenado es idéntico byte-a-byte al recibido (nunca recalculado) y el campo `tier` es el resultado de FL1 sobre ese mismo valor — ambos coexisten en el registro. + +### C. Estructura de la Reliquia (Regla 3) + +**AC-FL08 — `RelicRecord` contiene todos los campos requeridos con los valores transportados** **[Logic/unit]** +Dado un veredicto de entrada y una era `ACTIVE` conocida, cuando Forja construye la `RelicRecord`, entonces el registro resultante contiene exactamente `relic_id`, `source_hero_id`, `source_hero_name`, `relic_category`, `relic_quality`, `tier`, `origin_era_id`, `forge_index`, `epitaph` — sin campos faltantes, y los campos transportados (`source_hero_id`, `source_hero_name`, `relic_category`, `relic_quality`) son idénticos a los recibidos del veredicto. + +**AC-FL09 — `epitaph` se deriva de `relic_category × tier`; tabla incompleta → loud-fail al cargar** **[Logic/unit]** +Dado la tabla `epitaph_templates` con las 9 entradas requeridas —el producto cartesiano de las 3 categorías `{offensive, defensive, rally}` × los 3 tiers `{Delgada, Sólida, Legendaria}`— cuando se carga con cualquiera de esas 9 claves faltante o con string vacío, entonces la carga falla ruidosamente en build de desarrollo, nombrando la(s) clave(s) faltante(s) — nunca se forja una reliquia con un `epitaph` nulo o por defecto. + +### D. Índice de forja y `relic_id` estables (Regla 4, Fórmula FL2) + +**AC-FL10 — `forge_index` monótono desde 0 en un legado nuevo** **[Logic/unit]** +Dado un legado nuevo (sin guardado previo) donde mueren 3 héroes en secuencia, cuando cada uno se forja, entonces `forge_index` asignado es 0, 1, 2 en ese orden y `forge_counter` queda en 3. + +**AC-FL11 — `forge_counter` persistido continúa tras recarga, nunca reinicia ni reutiliza** **[Integration]** +Dado un `forge_counter` persistido en 3 tras una sesión previa (incl. a través de un cambio de era), cuando el juego recarga y se forja una 4ª reliquia, entonces `forge_index = 3` (continúa desde el valor persistido) — nunca vuelve a 0 ni reutiliza 0-2. + +**AC-FL12 — `relic_id` determinista y único, sin depender de IDs de instancia del engine** **[Logic/unit]** +Dado dos invocaciones de forja con el mismo `forge_counter` de entrada (misma posición del contador, sesiones simuladas distintas), cuando se deriva `relic_id`, entonces el resultado es idéntico entre ambas invocaciones (determinismo) y, para `forge_counter` distintos, `relic_id` nunca colisiona (unicidad) — mismo criterio anti-reciclaje que Kaiju `entity_id`. *(La cláusula negativa "nunca usa `get_instance_id()`" se enforza con un check estático de CI — ver nota de testabilidad al final.)* + +### E. Anti-nihilismo (Regla 5) + +**AC-FL13 — En el piso, el registro crece en exactamente una entrada (no cero)** **[Logic/unit]** +Dado un registro con `M` reliquias y un veredicto con `relic_quality = 0.10` (Tier Delgada, el peor caso anti-nihilismo), cuando Forja procesa el veredicto hasta `DEPOSITED`, entonces el conteo del registro es exactamente `M + 1` (nunca `M` — la muerte de tier bajo no se descarta ni se "salta") y la nueva entrada es recuperable por su `relic_id`. *(Complementa AC-FL03 [mapeo de tier en el piso] y AC-FL08 [forma completa del `RelicRecord`] con la aserción distinta de **membresía en el registro**: que una muerte desperdiciada efectivamente incrementa el panteón, no solo que produce un record bien formado.)* + +**AC-FL14 — `relic_quality` fuera de `[0.10, 1.00]` → loud-fail, nunca clamp** **[Logic/unit]** +Dado un veredicto con `relic_quality = 0.05` o `relic_quality = 1.05` (fuera de rango, contrato roto de Héroes), cuando Forja lo recibe, entonces falla ruidosamente en build de desarrollo — nunca clampa a un valor válido ni produce una reliquia "cero". + +### F. Depósito + autosave obligatorio (Regla 6, Estados) + +**AC-FL15 — Orden estricto: registro antes de autosave** **[Logic/unit]** +Dado una `RelicRecord` recién construida (`FORGED`), cuando Forja la deposita, entonces la llamada de adición al registro en memoria ocurre **antes** de la llamada que dispara el autosave (verificable con spies ordenados) — nunca al revés. + +**AC-FL16 — El juego no continúa hasta `SAVE_EXISTS`; la reliquia es irreversible tras `SAVING`** **[Integration]** +Dado una `RelicRecord` en `DEPOSITED` con el autosave disparado, cuando el autosave resuelve a `SAVE_EXISTS` (Guardado AC-02/AC-04), entonces la reliquia transiciona a `PERSISTED` y el flujo del juego continúa recién en ese punto — ningún consumidor observa la reliquia como definitiva antes de `SAVE_EXISTS`. + +**AC-FL17 — Fallo bloqueante de autosave deja la reliquia en `DEPOSITED`, sin reintento propio** **[Integration]** +Dado un autosave que agota sus reintentos y falla (Guardado AC-35), cuando Forja observa el fallo, entonces la `RelicRecord` permanece en `DEPOSITED` (no alcanza `PERSISTED`), el juego no continúa, y Forja no reintenta el guardado por su cuenta — delega enteramente en Guardado. + +### G. Alcance: Forja no posee efecto de combate ni exhibición (Regla 7) + +**AC-FL18 — Forja completa `RECEIVED→PERSISTED` sin que exista ningún consumidor de #12/#14** **[Integration]** +Dado un entorno de test sin ningún mock de Reliquias/Bendiciones ni de Salón Conmemorativo/Panteón conectado, cuando Forja procesa un veredicto completo, entonces alcanza `PERSISTED` exitosamente — Forja no requiere que #12/#14 existan para cumplir su propia responsabilidad (confirma el desacople estructural de la Regla 7 sin depender de una interfaz aún no diseñada). + +**AC-FL19 — `RelicRecord` expone `relic_category` + `tier` en forma consumible por Reliquias/Bendiciones** **(bloqueado — Reliquias/Bendiciones #12 sin GDD)** **[Integration]** +*Mock Contract Assumption: el mock de #12 expone un único hook de lectura `get_relic_combat_inputs() → {relic_category, tier}` sobre una `RelicRecord` dada. Re-verificar contra el GDD real de Reliquias/Bendiciones cuando exista.* +Dado una `RelicRecord` en `PERSISTED`, cuando el mock de #12 la consulta, entonces recibe `relic_category` y `tier` sin necesidad de leer `relic_quality` ni ningún otro campo interno — confirma que la superficie mínima que #12 necesitará ya está expuesta. + +**AC-FL20 — El registro de reliquias expone lectura consumible por el Panteón** **(bloqueado — Salón Conmemorativo/Panteón #14 sin GDD)** **[Integration]** +*Mock Contract Assumption: el mock de #14 expone un único hook de lectura `list_relics() → [RelicRecord]` sobre el registro completo. Re-verificar contra el GDD real de Panteón cuando exista.* +Dado un registro con N reliquias `PERSISTED`, cuando el mock de #14 lo consulta, entonces recibe la lista completa con `source_hero_name`, `relic_category`, `tier`, `epitaph`, `origin_era_id` de cada una — confirma que la superficie mínima de exhibición ya está expuesta. + +### H. Deduplicación defensiva (Regla 8) + +**AC-FL21 — Doble entrega del mismo `source_hero_id`+`origin_era_id` → no-op, nunca dos `relic_id`** **[Logic/unit]** +Dado un veredicto ya forjado para `source_hero_id=X` en `origin_era_id=Y`, cuando Forja recibe un segundo veredicto idéntico en `hero_id`+`origin_era_id`, entonces la segunda invocación es un no-op registrado con advertencia en build de desarrollo — el registro sigue conteniendo exactamente una `RelicRecord` para ese héroe en esa era, con un único `relic_id`. + +**AC-FL26 — `hero_id` único por roster de era → la clave de dedup es segura; roster con `hero_id` duplicado → loud-fail al activar la era** **[Logic/unit]** +Dado el roster de héroes de una era que se está activando (`LOADED`), cuando Forja (o el validador de carga) inspecciona los `hero_id` de ese roster, entonces: (a) si todos son únicos, la validación pasa —confirmando la precondición de unicidad de instancia sobre la que descansa la dedup de Regla 8 (dos muertes nunca comparten `(hero_id, origin_era_id)` salvo doble entrega, ya cubierta por AC-FL21)—; (b) si dos entradas del roster comparten `hero_id`, la activación de la era **falla ruidosamente** en build de desarrollo, nombrando el `hero_id` colisionante — nunca se permite un roster donde la dedup pudiera tragar silenciosamente una segunda muerte legítima. *(Inyectable vía el mismo `ActiveEraProvider` stub de AC-FL24: el roster se provee como fixture, desacoplado de Datos de Era real.)* + +### I. Edge Cases + +**AC-FL22 — Payload incompleto o nulo → loud-fail** **[Logic/unit]** +Dado un veredicto con cualquier campo faltante o nulo (`hero_id`, `hero_name`, `relic_category`, o `relic_quality`), cuando Forja lo recibe en `RECEIVED`, entonces falla ruidosamente en build de desarrollo — nunca construye una `RelicRecord` con datos incompletos ni sustituye un default silencioso. + +**AC-FL23 — `relic_category` desconocida → loud-fail** **[Logic/unit]** +Dado un veredicto con `relic_category` fuera de `{offensive, defensive, rally}`, cuando Forja lo recibe, entonces falla ruidosamente — nunca inventa ni sustituye una categoría por defecto. + +**AC-FL24 — Sin era `ACTIVE` al forjar → loud-fail** **[Logic/unit]** +Dado un momento de forja donde ninguna era está en estado `ACTIVE` —inyectado vía un **proveedor de era stubeado** (`ActiveEraProvider` mock que retorna `null`/none en lugar de leer Datos de Era real, mismo seam que Permadeath AC-P08b usa para su lectura de roster)— cuando Forja intenta etiquetar `origin_era_id`, entonces falla ruidosamente — nunca asigna un `origin_era_id` por defecto ni nulo. *(El seam concreto —parámetro inyectado vs. autoload stubeado— se fija con el patrón de implementación del ADR OQ-FL4; el AC solo exige que la lectura de era ACTIVE sea inyectable para poder forzar el caso "sin era" sin una era real cargada.)* + +**AC-FL25 — Recuperación por crash entre `RESOLVED` y el autosave de Forja: re-forja desde el payload de `DYING`, dedup evita el duplicado** **[Integration]** +Dado un crash ocurrido entre la confirmación `RESOLVED` de Permadeath y el fin del autosave de Forja (la `RelicRecord` nunca alcanzó `PERSISTED`), cuando el juego recarga, entonces el flujo de carga coacciona `DYING → DEAD` (Héroes AC-H27c / Permadeath Regla 8b) y re-forja la reliquia desde el payload persistido de `DYING`; la deduplicación de Forja (Regla 8) garantiza que, si la reliquia sí había alcanzado `PERSISTED` antes del crash, la re-forja es un no-op — el resultado final es exactamente una reliquia por héroe, nunca cero, nunca dos. + +### Clasificación de tipo de story (gate de evidencia) + +| AC # | Tipo | Evidencia | Gate | +|---|---|---|---| +| AC-FL02, FL03, FL04, FL05, FL06, FL07, FL08, FL09, FL10, FL12, FL13, FL14, FL15, FL21, FL22, FL23, FL24, FL26 | Logic | Unit test (`tests/unit/forja-de-legado/`) | Bloqueante | +| AC-FL01, FL11, FL16, FL17, FL18, FL25 | Integration | Integration test | Bloqueante | +| AC-FL19, FL20 | Integration (bloqueado) | Integration test con mock (Reliquias/Bendiciones #12 / Panteón #14 — sin GDD) | Bloqueante (hoy solo vía mock) | + +*(Nota: ningún AC de Forja depende de Permadeath, Guardado/Persistencia o Datos de Era/Civilización estando "sin GDD" — los tres ya tienen GDD, así que esos AC son Integration normal, pendientes solo de implementación, no de diseño aguas arriba. Solo AC-FL19/FL20 están bloqueados, y únicamente porque dependen de #12/#14.)* + +**Nota de testabilidad (checks estáticos de CI que complementan los unit tests):** + +*Ambos checks dependen de que el ADR de OQ-FL4 fije la ubicación del módulo de Forja (directorio/autoload) y confirme el nombre del símbolo de entrada. Hasta entonces, los targets abajo son **provisionales** — nombrados aquí para que el check sea accionable en cuanto exista el layout de código, no para bloquear su escritura hoy.* + +- **Regla 1 ("solo héroes forjan") y Regla 7 (ownership de alcance)** son afirmaciones de superficie/ownership, no de runtime — no hay input de "muerte de tropa" ni "efecto de combate" que Forja pueda rechazar en caja negra porque ese código no existe. AC-FL02 y AC-FL18 las cubren como auditoría de superficie y como aserción positiva de ciclo de vida; se complementan con un **grep estático de CI**: `grep -rn "receive_relic(" src/ --exclude-dir=` no debe arrojar ninguna invocación fuera del módulo de Permadeath (el único productor legítimo). *(Target provisional: símbolo `receive_relic`; ruta del módulo a fijar en OQ-FL4.)* +- **Regla 4 ("`relic_id` nunca de `get_instance_id()`")** es una cláusula negativa no verificable por el valor resultante. AC-FL12 prueba las propiedades observables (determinismo, unicidad); la cláusula negativa se enforza con un **grep estático de CI**: `grep -rn "get_instance_id()" ` no debe arrojar ninguna coincidencia. *(La "ruta del módulo de Forja" —p. ej. `src/legacy/forge/` o el archivo del autoload de forja— se fija en el ADR OQ-FL4; hasta que exista ese layout, el grep no tiene scope concreto y este check queda pendiente de OQ-FL4, no accionable hoy.)* + +## Open Questions + +**OQ-FL1 — ✅ RESUELTO 2026-08-17 (al pasar Forja a Approved).** +Forja lee qué `era_id` está `ACTIVE` (para `origin_era_id`) y `EraDefinition.legacy_props` referencia los `relic_id` que Forja produce — relación bidireccional. Reconciliación aplicada: (a) Datos de Era/Civilización ahora lista a Forja de Legado en su tabla de Dependientes (fila solo-lectura de era `ACTIVE`); (b) `systems-index.md` ya lista la fila de Forja con "Depends On: Permadeath, Guardado/Persistencia, Datos de Era/Civilización" (completa). Fue cosmético/estructural, sin cambio de diseño — mismo patrón que se resolvió para Permadeath↔Datos de Era el 2026-08-16. + +**OQ-FL2 — Contrato provisional con Reliquias/Bendiciones (#12, sin GDD).** +Forja expone `relic_category` + `tier` de cada `RelicRecord` como input del efecto de combate/bendición. La forma exacta (método de lectura, estructura del evento) es provisional (AC-FL19 bloqueado). **Acción**: confirmar al diseñar Reliquias/Bendiciones (#12) — es el consumidor crítico downstream que cierra la cadena Pilar 2 (muerte → reliquia → **poder en combate**). Forja define la forma del dato; #12 la respetará. + +**OQ-FL3 — Contrato provisional con Salón Conmemorativo/Panteón (#14, sin GDD).** +Forja expone el registro completo de reliquias para exhibición. La forma exacta de lectura es provisional (AC-FL20 bloqueado). **Acción**: confirmar al diseñar el Panteón (#14). + +**OQ-FL4 — Representación de la `RelicRecord`: ¿Resource (`.tres`) o dato serializado en el save? → becomes an ADR.** +El GDD define *qué campos* lleva una reliquia y *qué significan*, no *cómo se serializa*. Como el registro es estado mutable persistido (no dato autoral inmutable como `EraDefinition`), probablemente viva serializado dentro del save de Guardado (por referencia, Guardado Regla 4), no como `.tres` autoral. Pero la decisión concreta (estructura de datos, formato de serialización, dónde vive el registro en runtime — Autoload vs. nodo) es de arquitectura. **Acción**: crear un ADR con `/architecture-decision` antes de implementar, coordinado con el patrón de implementación de Permadeath (OQ-P5) y el esquema de save de Guardado. + +**Scope obligatorio del ADR (añadido en design-review 2026-08-17 — el `godot-specialist` flageó que estos tres puntos tocan directamente la garantía de persistencia y NO deben quedar implícitos):** +1. **Mismo frame, sin llamadas diferidas en la ruta depósito→autosave.** La garantía de orden de AC-FL15 (añadir-al-registro **antes** de disparar el autosave) se rompe si el depósito o el disparo usan `call_deferred()` / `CONNECT_DEFERRED`: el orden se vuelve no determinista entre frames y el test con spies pasaría en aislamiento mientras el hand-off real corre en carrera. El ADR debe fijar que la mutación del registro y el disparo del autosave ocurren en el **mismo call frame**, sin llamadas diferidas en esa ruta. +2. **`assert()` no basta para la validación que cruza frontera de confianza.** Los loud-fail del GDD (payload incompleto, `relic_quality` fuera de rango, `relic_category` desconocida, `epitaph_templates` incompleta) validan datos que **llegan de otros sistemas** (no bugs internos). Como `assert()` se compila fuera en builds exportados, confiar solo en él enviaría **cero validación** al jugador → `RelicRecord` malformadas silenciosas o crashes por claves faltantes, socavando la propia garantía anti-nihilismo. El ADR (o una nota de coding-standard) debe especificar el patrón: `assert()` (señal ruidosa en dev) **emparejado con** un `if`-guard + `push_error()` + ruta de fallo seguro que **permanezca activa en release**. +3. **Serialización de `forge_counter` como `int64`.** Ver la nota de serialización en Edge Cases: si se requiere fidelidad de 64 bits como contrato, usar `FileAccess.store_var`/`get_var` (no `JSON`, que trunca a 53 bits). Coordinar con el formato de save de Guardado (hoy no especificado en su GDD). +4. **Bloqueo no-congelante en el autosave (Regla 6).** "El juego no continúa hasta `SAVE_EXISTS`" debe implementarse como `await` sobre la señal de completado de Guardado (cede al main loop, mantiene el render del indicador "guardando…"), **nunca** como ejecución literalmente síncrona ni `OS.delay_msec()`. La sección "States and Transitions" describe el proceso como "esencialmente síncrono" — el ADR debe aclarar que eso significa una corrutina gateada por `await`, no un bloqueo del hilo. + +**OQ-FL5 — Esquema exacto de *encoding* de `relic_id` desde `forge_index`.** +La Regla 4 fija que `relic_id` se deriva de forma determinista y única del `forge_index` (nunca de `get_instance_id()`), pero no fija el *formato* exacto (¿`"relic_%d" % forge_index`? ¿`"{origin_era_id}:{forge_index}"`? ¿un hash estable?). **Acción**: fijar en el mismo ADR de OQ-FL4. **Restricciones de diseño ya fijadas (design-review 2026-08-17, ver Regla 4):** tipo **`String`**; **único en todo el legado** (global al save); **estable** a través de guardados/cargas. Solo resta la elección del *encoding* concreto, que es de implementación. diff --git a/design/gdd/game-concept.md b/design/gdd/game-concept.md new file mode 100644 index 0000000000..bc4bf85f8d --- /dev/null +++ b/design/gdd/game-concept.md @@ -0,0 +1,271 @@ +# Game Concept: What the Gods Left Behind + +*Created: 2026-07-20* +*Status: Draft* + +--- + +## Elevator Pitch + +> Es un RTS narrativo con elementos roguelite donde comandas civilizaciones distintas a lo largo de la historia, cada una enfrentando a un kaiju que amenaza con destruirla — y las muertes permanentes de tus héroes se convierten literalmente en las reliquias y bendiciones que la siguiente civilización usará para sobrevivir a la próxima bestia. +> +> Test de 10 segundos: comandas un héroe y su ejército contra un kaiju gigante; si tu héroe muere, esa muerte se vuelve un arma legendaria para la próxima era. Se entiende de inmediato. + +--- + +## Core Identity + +| Aspect | Detail | +| ---- | ---- | +| **Genre** | RTS narrativo / Roguelite de meta-progresión | +| **Platform** | Multiplataforma (PC como plataforma principal, consola como objetivo posterior) | +| **Target Audience** | Fans de campañas narrativas RTS (Warcraft 3, Age of Mythology) y de roguelites con permadeath significativo (Hades, Darkest Dungeon) | +| **Player Count** | Un jugador | +| **Session Length** | 30-120 min (una "era"/campaña corta por sesión) | +| **Monetization** | Premium (pago único) | +| **Estimated Scope** | Grande (15-21 meses, en solitario) | +| **Comparable Titles** | Warcraft 3 (campaña con héroes), Age of Mythology (civilizaciones/mitología jugable), Hades (meta-progresión entre corridas con permadeath significativo) | + +--- + +## Core Fantasy + +Eres el líder que la Historia recuerda por haber pagado el precio necesario para que la humanidad sobreviviera un poco más. No ganas simplemente batallas: decides quién muere, cómo muere, y qué queda de esa muerte para que la siguiente generación pueda pelear. Es la fantasía de ser Arthas, Ezio o Aquiles en el momento exacto en que la tragedia se vuelve mito — y de ver esa tragedia convertirse, generaciones después, en la espada que otro héroe empuña. + +--- + +## Unique Hook + +Es como Age of Mythology (civilizaciones con poder mitológico jugable), Y ADEMÁS los "mitos" que usas en combate son literalmente las decisiones y muertes de tus propias partidas anteriores — cada reliquia legendaria tiene el nombre de un héroe que tú viste morir, y su poder mecánico refleja quién fue y cómo cayó (su rol define el tipo de bendición; qué tan bien se gastó su muerte define su potencia). + +Esto afecta directamente el gameplay (no es solo lore): el pool de reliquias/bendiciones disponibles en una era depende enteramente de cómo jugó el usuario las eras anteriores. + +> **Precisión de fidelidad (Pilar 2):** el poder de una reliquia refleja **quién fue el héroe y cómo cayó** — su rol/arquetipo determina el *tipo* de bendición (ofensiva/defensiva/rally, `relic_category`), y qué tan bien se gastó su muerte (deliberada, en juego alto, sirviendo un propósito) determina su *potencia* (tier). No es que cada muerte genere un efecto único e irrepetible; es que cada reliquia lleva el nombre y la circunstancia de una tragedia que el jugador vivió, y su fuerza mecánica está atada a esa circunstancia. + +--- + +## Player Experience Analysis (MDA Framework) + +### Target Aesthetics (What the player FEELS) + +| Aesthetic | Priority | How We Deliver It | +| ---- | ---- | ---- | +| **Sensation** (sensory pleasure) | 2 | Pixel art gótico de alto detalle, clímax de combate contra kaijus con contraste de escala brutal | +| **Fantasy** (make-believe, role-playing) | 3 | El jugador ES el líder histórico cuyas decisiones se vuelven mito | +| **Narrative** (drama, story arc) | 1 | Estructura de tragedia por era; legado mecánico directo entre campañas | +| **Challenge** (obstacle course, mastery) | 4 | Combate táctico deliberado, gestión de reliquias limitadas | +| **Fellowship** (social connection) | N/A | Sin foco multijugador en el MVP | +| **Discovery** (exploration, secrets) | 5 | Descubrir qué reliquia nace de cada tipo de muerte/decisión | +| **Expression** (self-expression, creativity) | 6 | Construcción de un panteón propio de reliquias a través de las eras | +| **Submission** (relaxation, comfort zone) | N/A | No es un objetivo de diseño | + +### Key Dynamics (Emergent player behaviors) +- Los jugadores empezarán a tomar decisiones tácticas pensando en "qué reliquia quiero que nazca de esto", no solo en ganar la batalla inmediata. +- Los jugadores se encariñarán con héroes sabiendo que su muerte es probable y mecánicamente valiosa, generando apego emocional real en lugar de frustración por permadeath. +- Los jugadores compararán y compartirán los panteones únicos de reliquias que construyeron a partir de sus propias decisiones. + +### Core Mechanics (Systems we build) + +1. **Combate ritual-clímax**: fases de preparación/posicionamiento pausadas que desembocan en enfrentamientos breves e intensos contra el kaiju de la era. +2. **Permadeath con forja de legado**: la muerte de un héroe es permanente y genera mecánicamente una reliquia/hechizo específico basado en quién fue y cómo cayó (rol → tipo; calidad de la muerte → potencia), disponible en eras futuras. +3. **Bendiciones aleatorias por reliquia**: durante el combate se ofrecen bendiciones sorteadas desde el panteón de reliquias acumuladas, generando variedad de build por partida (estilo "boons" de Hades). +4. **Transición narrativa entre eras**: cada civilización/era es una campaña acotada que termina en una decisión de consecuencia permanente, conectando mecánica y narrativamente con la siguiente era. + +--- + +## Player Motivation Profile + +### Primary Psychological Needs Served + +| Need | How This Game Satisfies It | Strength | +| ---- | ---- | ---- | +| **Autonomy** (freedom, meaningful choice) | El jugador decide activamente qué héroe sacrifica, cómo, y qué reliquia se forja de esa decisión | Core | +| **Competence** (mastery, skill growth) | Dominio táctico de invocar bendiciones/reliquias en el momento correcto del clímax contra el kaiju | Core | +| **Relatedness** (connection, belonging) | Vínculo emocional fuerte con héroes que probablemente no sobrevivirán, y con el panteón histórico que el jugador mismo construyó | Supporting | + +### Player Type Appeal (Bartle Taxonomy) + +- [x] **Achievers** (goal completion, collection, progression) — How: construir un panteón completo de reliquias legendarias a través de todas las eras +- [x] **Explorers** (discovery, understanding systems, finding secrets) — How: descubrir qué reliquia/bendición nace de cada tipo de muerte o decisión táctica +- [ ] **Socializers** (relationships, cooperation, community) — No es un foco del MVP +- [ ] **Killers/Competitors** (domination, PvP, leaderboards) — No es un foco del MVP; explícitamente excluido (ver Anti-Pilares) + +### Flow State Design + +- **Onboarding curve**: la primera era/civilización enseña el loop completo (preparación → clímax → decisión de legado) en una sola campaña corta, sin exponer aún el sistema de bendiciones aleatorias hasta la segunda partida. +- **Difficulty scaling**: cada kaiju siguiente exige combinaciones más específicas de reliquias heredadas, empujando al jugador a planear decisiones de sacrificio con más cuidado. +- **Feedback clarity**: cada reliquia lleva el nombre y la escena de la muerte que la originó, haciendo visible de inmediato la conexión entre decisión pasada y poder presente. +- **Recovery from failure**: la derrota (o la muerte de un héroe) nunca es un game-over — se transforma automáticamente en contenido jugable futuro, haciendo del fracaso una fuente narrativa y mecánica, no un castigo vacío. + +--- + +## Core Loop + +### Moment-to-Moment (30 seconds) +Fases de posicionamiento y preparación ritual (mover tropas, invocar bendiciones limitadas) que estallan en choques breves, densos y de alta tensión contra el kaiju — el ritmo alterna deliberadamente entre calma táctica y clímax explosivo. + +### Short-Term (5-15 minutes) +Ciclos de "avance de era": recolectar recursos y posicionar fuerzas mientras el kaiju se acerca en un temporizador visible, decidiendo cuándo invocar una reliquia heredada (recurso limitado, no se puede abusar). + +### Session-Level (30-120 minutes) +Una campaña-era completa (una civilización, un kaiju) que siempre termina en una decisión de consecuencia permanente: qué héroe muere, cómo muere, y qué reliquia se forja de esa muerte para la siguiente era. + +### Long-Term Progression +El jugador construye su propio panteón de reliquias legendarias a través de las eras de la historia. El objetivo final no es "ganar" una batalla individual, sino completar la cadena histórica completa y ver qué mitología terminó construyendo con sus propias decisiones. + +### Retention Hooks +- **Curiosity**: qué reliquia nacerá de la próxima muerte, y cómo cambiará la próxima era. +- **Investment**: héroes por los que el jugador desarrolló apego emocional, y el panteón acumulado que no quiere "perder" abandonando el juego. +- **Social**: (secundario) comparar el panteón único construido con el de otros jugadores. +- **Mastery**: dominar la sincronización de bendiciones y reliquias en el clímax de combate contra kaijus cada vez más exigentes. + +--- + +## Game Pillars + +### Pillar 1: Sacrificio con Peso +La muerte de un héroe es permanente y define el futuro del juego, nunca un simple game-over. + +*Design test*: Si dudamos entre dar un reintento gratuito o dejar que la caída del héroe sea definitiva y genere legado, elegimos que sea definitiva. + +### Pillar 2: El Pasado es Poder +Las decisiones y muertes de campañas anteriores se convierten literalmente en herramientas mecánicas (reliquias, hechizos, bendiciones) en el futuro. + +*Design test*: Si un sistema nuevo no puede conectarse con claridad a una decisión pasada del jugador, no pertenece al juego. + +### Pillar 3: Preparación Ritual, Clímax Explosivo +El ritmo alterna entre construcción/posicionamiento pausado y enfrentamientos breves e intensos contra el kaiju. + +*Design test*: Si una mecánica alarga el combate sin aumentar la tensión, se recorta. + +### Pillar 4: Historia Jugada, No Contada +Los momentos trágicos se viven a través de decisiones tácticas del jugador, no de cinemáticas pasivas. + +*Design test*: Si un momento narrativo clave puede resolverse sin que el jugador tome una decisión, se rediseña para que dependa de él. + +### Anti-Pillars (What This Game Is NOT) + +- **NOT multijugador competitivo en el MVP**: comprometería el Pilar 2, ya que la progresión de legado es un viaje personal del jugador con su propio panteón. +- **NOT construcción de base/economía masiva estilo Age of Empires clásico**: comprometería el Pilar 3; el ritmo se diluiría con micromanagement económico extenso. +- **NOT generación procedural de mapas o civilizaciones completas**: comprometería el Pilar 4; la narrativa autoral se perdería en contenido puramente aleatorio. La aleatoriedad se limita a las bendiciones/reliquias ofrecidas en combate, no a la estructura narrativa de las eras. + +--- + +## Visual Identity Anchor + +**Dirección visual seleccionada**: Gótico Pixelado Sagrado (referencia directa: Blasphemous 2) + +**Regla visual de una línea**: Cada escena debe leerse como un fresco de vitral renderizado en píxeles — ornamentado, oscuro y reverente. + +**Principios visuales de soporte**: +1. Pixel art de alto detalle con iluminación pictórica. *Design test*: si un sprite se ve genérico o sin ornamento, se rehace. +2. Contraste de escala brutal entre los kaijus y las unidades pequeñas con armadura gótica. *Design test*: si el kaiju no empequeñece la pantalla en un encuentro de clímax, la composición falla. +3. Color como señal ritual. *Design test*: si un elemento legendario (reliquia, bendición) no resalta claramente contra el mundo apagado, no está cumpliendo su función. + +**Filosofía de color**: Paleta apagada de piedra, sangre seca y hueso como base por civilización/era, reservando acentos dorados y luminosos exclusivamente para reliquias y elementos legendarios — reforzando mecánica y visualmente que el legado es literalmente lo más sagrado y brillante del mundo del juego. + +--- + +## Inspiration and References + +| Reference | What We Take From It | What We Do Differently | Why It Matters | +| ---- | ---- | ---- | ---- | +| Warcraft 3 | Héroes centrales con arcos narrativos trágicos, combate táctico deliberado con pocas unidades | El arco trágico de un héroe se vuelve mecánicamente heredable, no solo un recuerdo narrativo | Valida que el público de RTS ya ama este tipo de peso narrativo | +| Age of Mythology | Civilizaciones distintas con poder mitológico jugable | La mitología no es fija/histórica: es literalmente generada por las partidas pasadas del jugador | Valida el atractivo de facciones históricas con poder sobrenatural jugable | +| Hades / Rogue Legacy | Meta-progresión entre corridas, permadeath con propósito, bendiciones aleatorias | Sin estructura de "corridas" cortas repetidas: las eras son campañas narrativas largas y acotadas, no runs infinitos | Valida que el permadeath con recompensa mecánica genera apego, no frustración | + +**Non-game inspirations**: La caída de Arthas en Warcraft 3, la muerte de Cristina en Assassin's Creed Brotherhood, y el sacrificio de Quirón en Age of Mythology — todos comparten el patrón de tragedia personal dentro de un conflicto histórico más grande, que es el núcleo emocional de este juego. + +--- + +## Target Player Profile + +| Attribute | Detail | +| ---- | ---- | +| **Age range** | 18-40 | +| **Gaming experience** | Mid-core a hardcore | +| **Time availability** | Sesiones de 30-120 minutos, capaces de completar una campaña-era por sesión | +| **Platform preference** | PC primero, consola después | +| **Current games they play** | Warcraft 3 (Reforged/clásico), Age of Mythology, Hades, Darkest Dungeon | +| **What they're looking for** | Un RTS con el peso narrativo y las consecuencias permanentes que sienten que faltan en el género desde Warcraft 3 | +| **What would turn them away** | Enfoque competitivo/PvP, micromanagement económico pesado sin recompensa narrativa, permadeath sin recompensa mecánica clara | + +--- + +## Technical Considerations + +| Consideration | Assessment | +| ---- | ---- | +| **Recommended Engine** | Godot 4.6 (ya pineado en el proyecto; buen soporte 2D, gratuito, adecuado para el alcance de un desarrollador solo) | +| **Key Technical Challenges** | Persistencia de datos de legado entre "eras"/campañas; balance del pool de reliquias/bendiciones a medida que crece; UI clara para mostrar la procedencia narrativa de cada reliquia | +| **Art Style** | Pixel art gótico de alto detalle (2D) | +| **Art Pipeline Complexity** | Media-alta (animación cuidadosa de sprites, iluminación pictórica, sin necesidad de modelado 3D) | +| **Audio Needs** | Moderado a alto — música orquestal/coral que refuerce el tono trágico, diseño de sonido de impacto para los clímax contra kaijus | +| **Networking** | Ninguno en el MVP (un jugador) | +| **Content Volume** | Visión completa: 4-5 civilizaciones/eras jugables, 4-5 kaijus únicos, estimado de 15-20 horas de campaña total | +| **Procedural Systems** | Limitado — solo el sorteo de bendiciones/reliquias en combate es aleatorio; mapas y estructura narrativa son autorales | + +--- + +## Risks and Open Questions + +### Design Risks +- El sistema de legado directo/mecánico (reliquias con nombre propio por muerte de héroe) podría volverse difícil de balancear si el pool crece sin control entre eras. +- El apego emocional a los héroes depende de una escritura y presentación fuertes; si la narrativa no logra ese peso, el permadeath se siente como un castigo vacío en vez de una tragedia significativa. + +### Technical Risks +- Godot 4.6 es una versión reciente (post-cutoff del modelo) — hay que validar patrones de UI y persistencia de guardado entre "eras" temprano, cruzando referencia con `docs/engine-reference/godot/VERSION.md`. +- El sistema de persistencia de legado entre campañas es una arquitectura de datos nueva sin muchos referentes RTS directos. + +### Market Risks +- El género de RTS narrativo de un jugador es nicho; el público existe (fans de Warcraft 3/AoM) pero es más pequeño que el RTS competitivo tradicional. +- La combinación con permadeath/roguelite podría alejar a jugadores de RTS tradicionales que esperan poder "guardar y cargar" sin penalización. + +### Scope Risks +- El Alpha (4-5 civilizaciones completas) es el tramo de mayor riesgo de tiempo para un desarrollador solo. +- El arte pixel gótico de alto detalle es lento de producir en solitario y podría extender el pipeline de arte más de lo estimado. + +### Open Questions +- ¿Cuántas reliquias/bendiciones por era son suficientes para sentir variedad sin volverse inmanejables de balancear? — a resolver con `/prototype` del sistema de legado. +- ¿Cómo se comunica visualmente en la UI que una reliquia proviene de una decisión pasada del jugador y no de contenido genérico? — a resolver con `/art-bible` y `/ux-design`. + +--- + +## MVP Definition + +**Core hypothesis**: Los jugadores encuentran satisfactorio el ciclo de preparación ritual → clímax contra el kaiju → consecuencia permanente de legado, incluso dentro de una sola era. + +**Required for MVP**: +1. Una civilización jugable completa (una era) con su propio kaiju y clímax de combate. +2. Sistema de permadeath de héroe funcionando de principio a fin, incluyendo la **producción** de la reliquia: una muerte presenciada por el jugador se forja en una `RelicRecord` de extremo a extremo (permadeath → forja de legado). Esta es la mitad del Pilar 2 que una sola era **sí** puede validar. +3. El **consumo** de reliquias (invocar bendiciones durante el clímax, el draft 3-choose-1) validado con un **panteón pre-sembrado** de héroes de una "era 0" establecida en la ficción. La otra mitad del Pilar 2 —*invocar tu propia muerte presenciada en una era anterior*— es inherentemente cross-era y se difiere al **Vertical Slice** (2 eras conectadas), donde ese loop realmente vive. Esto es coherente con la curva de onboarding del propio concepto ("las bendiciones aleatorias no se exponen hasta la 2ª partida"). + +> **Nota de scope (reconciliación cross-review 2026-08-18):** el requisito de "2-3 reliquias heredables" del MVP **no** significa invocar reliquias de héroes muertos en la misma era/pelea — `reliquias-bendiciones.md` Regla 10 prohíbe el drafteo intra-era (preserva el peso trágico: no draftear a quien murió hace 90s). El MVP prueba la *producción* de reliquias desde muertes presenciadas (in-era) y el *feel de invocación* (con panteón pre-sembrado) por separado; la unión de ambos —presenciar una muerte y usar esa reliquia más tarde— es la promesa del Vertical Slice. + +**Explicitly NOT in MVP** (defer to later): +- Sistema multi-era / transición de legado entre civilizaciones (se prueba en Vertical Slice). +- Arte final pulido para más de una civilización. +- Cualquier funcionalidad multijugador o de comparación social de panteones. + +### Scope Tiers (if budget/time shrinks) + +| Tier | Content | Features | Timeline | +| ---- | ---- | ---- | ---- | +| **MVP** | 1 civilización, 1 kaiju | Loop ritual→clímax + permadeath + legado básico (2-3 reliquias) | 3-4 meses | +| **Vertical Slice** | 2 civilizaciones/eras conectadas | + transición de legado entre eras | 2-3 meses adicionales | +| **Alpha** | 4-5 civilizaciones (todas las eras), arte placeholder en zonas nuevas | Todos los sistemas, panteón completo | 6-8 meses adicionales | +| **Full Vision** | Todo pulido, arte final, narrativa completa (15-21 meses, solo) | Todo + balance + polish | 4-6 meses adicionales | + +--- + +## Next Steps + +- [ ] Get concept approval from creative-director +- [ ] Fill in CLAUDE.md technology stack based on engine choice (`/setup-engine`) +- [ ] Create game pillars document (`/design-review` to validate) +- [ ] **Prototype core idea** (`/prototype [core-mechanic]`) — before writing GDDs, validate the concept is worth designing +- [ ] If prototype PROCEEDS: Decompose concept into systems (`/map-systems`) +- [ ] Design each system (`/design-system [system-name]`) — use prototype learnings in Tuning Knobs and Formulas sections +- [ ] Build vertical slice in Pre-Production (`/vertical-slice`) — validate full game loop before committing to Production +- [ ] Validate core loop with playtest (`/playtest-report`) +- [ ] Plan first milestone (`/sprint-plan new`) diff --git a/design/gdd/gdd-cross-review-2026-08-18.md b/design/gdd/gdd-cross-review-2026-08-18.md new file mode 100644 index 0000000000..52b62706b8 --- /dev/null +++ b/design/gdd/gdd-cross-review-2026-08-18.md @@ -0,0 +1,164 @@ +# Cross-GDD Review Report + +**Date:** 2026-08-18 +**GDDs Reviewed:** 13 system GDDs + concept + registry +**Systems Covered:** Input, Datos de Era, Guardado, Control, Tropas, Héroes, Temporizador, Combate, Encuentro con Kaiju, Permadeath, Forja de Legado, Reliquias/Bendiciones, UI/HUD + +> **✅ Reporte completo.** Los tres pases corrieron: **Holismo de Diseño (Fase 3)** el 2026-08-18 (abajo, con adenda de resolución de la misma fecha); **Consistencia cross-GDD (Fase 2)** y **Walkthrough de escenarios (Fase 4)** re-corridos en sesión limpia el 2026-08-18 (2 agentes de consistencia en paralelo + 1 de escenarios). La consistencia de VALORES está adicionalmente cubierta por el `/consistency-check` dedicado del mismo día (PASS, 0 conflictos — `docs/consistency-report-2026-08-18.md`). **Veredicto consolidado: FAIL — no avanzar a `/create-architecture` hasta resolver los bloqueantes (ver Verdict al final).** +> +> *Nota de orden de lectura: las secciones aparecen como Fase 3 (holismo) → Fase 2 (consistencia) → Fase 4 (escenarios) → GDDs flaggeados → Verdict consolidado → Adenda de resolución de Fase 3. Los ids de hallazgo de Fase 2 usan prefijo `F2-`; los de Fase 4 usan `S#`; los de Fase 3 usan `B#/W#`.* + +--- + +## Game Design Issues (Fase 3 — completa) + +### 🔴 Blocking + +**B1 — La Regla 10 de Reliquias contradice el requisito #3 del MVP.** +`reliquias-bendiciones.md` Regla 10 (aplicada 2026-08-18) prohíbe que las muertes intra-era entren al pool. `game-concept.md` MVP Definition requiere "2-3 reliquias heredables generadas a partir de la muerte del héroe" en una **sola era**. Mutuamente excluyentes. El workaround (pre-sembrar panteón "era 0", OQ-RB5) hace que el MVP demuestre la invocación con reliquias de héroes que el jugador nunca vio morir — debilitando el design-test de la propia fantasía. *Nota: esta tensión es consecuencia directa del fix del bloqueante #4 del design-review; requiere decisión de scope MVP.* + +**B2 — Sobrecarga de atención en CLIMAX (~6-8 sistemas activos vs. techo 3-4).** Control de unidades, telegrafío del kaiju, readout de peligro ×3 (`ui-hud.md` OQ-U8 sin resolver), esbirros, decisiones P de marcado, timing de Sacrificio, Furia (b), y el draft 3-choose-1 + cargas + band_headroom + timers. La cámara lenta (Regla 7b) mitiga la *aditividad* del draft pero no la densidad base. `ui-hud.md` OQ-U8 predata este GDD y su fila de dependencias aún dice Reliquias "sin GDD" — nunca contó el draft. + +**B3 — "Farmear muertes" es la meta óptima; Furia la premia.** Cada muerte → 1 reliquia permanente (`forja-de-legado.md` Regla 5); héroes resetean por era sin costo cross-era; muertes deliberadas tardías dan Sólida/Legendaria fiable. Peor: la barra de Furia (b) se calcula sobre héroes **vivos** (`encuentro-con-kaiju.md` Regla 10b), así que matar héroes relaja la presión anti-turtle. Cross-GDD (Encuentro × Permadeath × Forja). + +**B4 — Reliquias = fuente infinita sin sumidero.** `pool_cap=16` FIFO es throttle de visibilidad, no sumidero (AC-RB38: RelicRecord nunca se borra). Toda la escasez la carga `invocation_charges`. *(Nota del revisor senior: parcialmente by-design — el panteón es una colección que crece, no un consumible; el problema real es B3, el farmeo, no la acumulación en sí. La retórica "finitas, no farmeables" del concepto sí es engañosa y conviene reencuadrarla.)* + +**B5 — `relic_category` es pre-autorado en `HeroDefinition`**, fijado antes de que el héroe pelee (`sistema-de-heroes.md` Regla 1/AC-H06). El Unique Hook dice "su poder mecánico refleja exactamente cómo murió" — pero solo el *tier* (magnitud) refleja la muerte; la *categoría* (tipo de efecto) refleja quién era el héroe. Con OQ-RB10 (9 combos + epitafios de plantilla), la capa mecánica se acerca a rareza ARPG diferenciada por retrato. *(Nota senior: lectura defendible — categoría = identidad del héroe, tier = calidad de la muerte; pero el lenguaje del concepto promete más. Decisión a nivel concepto.)* + +### ⚠️ Warnings + +- **W1** — Anclaje ofensivo/defensivo unilateral (cuenta golpes comprados, no golpes esperados recibidos en la ventana); `rally` sin ancla y a menudo lee 0 vía band_headroom → el 3-choose-1 degrada a 2 opciones vivas. OQ-RB8d insuficiente por sí solo. +- **W2** — Peor caso F-RB1 (25→79 con 3 Legendarias ofensivas distintas) excede el techo de legibilidad 43 que el propio argumento de anclaje asume. +- **W3** — Legendaria `rally` (5 unidades instantáneas) bypassa la economía de attrition que protege `reinforcement_ratio` (`sistema-de-tropas.md`). +- **W4** — Nadie posee el escalado cross-era kaiju-vs-pool ("cada kaiju exige combinaciones más específicas"); Transición de Era #15 Not Started. +- **W5** — Fantasía de "drafter Hades" de Reliquias en tensión con la identidad deliberada-reverente de los otros GDDs; la cámara lenta es un parche UX, no una reconciliación de fantasía. +- **W6** — `game_time_paused` (Permadeath Regla 4, stop total) × `offering_time_scale` (Reliquias 7b, nunca pausa) sin contrato de interacción; un héroe que muere durante `OFFERING` es edge case reconocido. OQ-RB9 no incluye a Permadeath. + +--- + +## Consistency Issues (Fase 2 — COMPLETA, re-corrida 2026-08-18 sesión limpia) + +Re-corrida con 2 agentes paralelos (wiring: bidireccionalidad 2a / stale-refs 2c / ownership 2d — general-purpose; semántica: contradicciones de reglas 2b / rangos de fórmula 2e / cross-check de ACs 2f — systems-designer). Cobertura completa, sin truncar. Los hallazgos se agrupan aquí por causa raíz; varios fueron detectados independientemente por ambos agentes (convergencia = señal fuerte). El `/consistency-check` dedicado del día ya confirmó que los **valores registrados no tienen conflicto** (PASS) — esta fase cubre estructura (wiring) y semántica (reglas/fórmulas/ACs), no valores. + +### 🔴 Blocking + +**F2-B1 — El hook de daño del Pilar 2 no tiene dueño (detectado por 2A-consistencia-C2/D4 y 2B-semántica-B1).** +`effective_attack_damage()` aparece **11× en `reliquias-bendiciones.md`, 0× en `combate-dano.md`**. El `CombatProfile` de Combate es estrictamente **por tipo** (F1 lee un `attack_damage` estático); no existe capa de modificador de instancia en ninguna parte. El peor caso que la propia Reliquias computa (`25 → 79` con 3 Legendarias ofensivas, F-RB1) **no puede fluir por la fórmula de daño de Combate tal como está escrita**. El registro lo anota `source: reliquias / referenced_by: combate # PROVISIONAL`, pero es aspiracional — el modelo de tuning de Combate (solo-por-tipo, explícito en sus Tuning Knobs) lo contradice. **Requiere un ADR que decida quién posee la capa de instancia antes de implementar cualquiera de los dos sistemas.** OQ-RB1 abierta. + +**F2-B2 — Dos GDDs *Approved* especifican superficies de API opuestas (2A-C1).** +Forja dice que #12 y #14 *exponen* `get_relic_combat_inputs()` / `list_relics()`; Reliquias dice que **Forja** los expone y #12 los *llama*. Ambos son read-hooks sobre el registro `RelicRecord` de Forja (la lectura de Reliquias es la coherente), pero el diseño de check estático de CI de Forja está construido sobre su versión invertida. Hay que elegir un dueño. + +**F2-B3 — Reliquias es huérfana en el wiring de ≥5 vecinos (2A-A1, confirma OQ-RB3).** +Reliquias reclama 7 dependencias upstream; **5 filas recíprocas de dependiente faltan genuinamente** — Temporizador, Tropas, Héroes, Guardado y Datos de Era nunca la reconocen. La mitad de `systems-index.md` (tabla de enumeración) ya está bien; faltan solo las filas del lado GDD. Verbatim de las filas a reciprocar: Reliquias L249–253. + +**F2-B4 — La arista Reliquias↔Permadeath no está declarada en ninguna tabla, y nadie posee el reloj `offering_time_scale` (2A-A2/D2, 2B-W3).** +La precedencia de pausa Permadeath (`game_time_paused`, stop total) sobre la cámara lenta de la oferta (`offering_time_scale`, nunca pausa) está escrita solo en OQ-RB9 de Reliquias, no en Reglas, y su propio texto dice que "bloquea la implementación". La lista de consumidores de `game_time_paused` de Permadeath Regla 4 nombra solo Combate y Kaiju — **no Reliquias**. Y `offering_time_scale` no tiene dueño: Reliquias OQ-RB9 admite que "solicita" pero "no posee" el reloj de simulación; ningún otro GDD lo reclama; no está en `entities.yaml`. Ver también Fase 4 S2/S6. + +**F2-B5 — `band_headroom` / tope de ejército indefinido + colisión de faucets de refuerzo (2A-D3, 2B-W4).** +La bendición `rally` y su UI RU-3b dependen de un concepto de capacidad de banda que **no existe** en Tropas ni Datos de Era (grep: cero matches). El único concepto adyacente, `max_reinforcements_[tipo]`, está scopeado a reclutamiento por-tipo en fase PREPARATION — pero `rally` es solo-CLIMAX. Además `rally_reinforce_base` (Reliquias) y `reinforcement_ratio` (Tropas) son dos faucets a la misma banda sin reconciliación. Ver Fase 4 S5. + +**F2-B6 — El "Dependency Map" de `systems-index.md` contradice su propia tabla de enumeración (2A-A7).** +Mismo archivo, dos secciones, dos grafos: el Map omite Datos-de-Era de Permadeath, 5 deps de Reliquias, Permadeath de Kaiju, etc. — 8 sistemas afectados. Y "Circular Dependencies: ninguna encontrada" no está soportado (≥5 pares se auto-describen `Dura (bidireccional)`). Un arquitecto no puede saber cuál grafo es autoritativo. + +**F2-B7 — Batch de "sin GDD / bloqueado" stale que gatea ACs vivas (2A-2c).** +Numerosos marcadores "sin GDD" ahora son **factualmente falsos** y dejan ACs mockeadas que deberían estar desbloqueadas: Forja↔Reliquias (AC-FL19, OQ-FL2), Permadeath↔Forja (AC-P16, OQ-P3), Combate↔Permadeath (AC-C52/C42), Héroes/Tropas/Kaiju/Temporizador↔UI-HUD, y varias filas de dep bidireccionales. `encuentro-con-kaiju.md` L306 y `ui-hud.md` L167 **ya diagnosticaron su propia obsolescencia y el fix nunca corrió**. Mecánico de arreglar, pero gatea ACs → resolver antes de arquitectura. Lista completa: 2A sección 2c (15 ítems blocking + ~15 warning). *(NO flaggear: referencias a Panteón #14, Transición #15, Audio #16, Narrativa #17, Accesibilidad #18 como "sin GDD" — verificadas correctas, deben quedarse.)* + +### ⚠️ Warnings / Concerns + +- **F2-W1 — Ancla defensiva se rompe bajo Furia (2B-C3).** La garantía "Legendaria defensiva compra estrictamente >1 golpe de kaiju": +68 ÷ 68 (daño Furia) = **exactamente 1.0**, viola el invariante. Ninguno de los dos docs se cruza con el otro. +- **F2-W2 — `kaiju_max_health` indefinido en todo el corpus (2B-C4, ya OQ-1 de Kaiju).** No se puede evaluar si bendiciones apiladas trivializan la victoria de era. Mantener abierto hasta autorar `kaiju_definition.max_health`. +- **F2-W3 — Combate Regla 5 es silente sobre Furia (2B-W1).** Kaiju llama a la escala de Furia "la única excepción documentada de Combate Regla 5", pero Combate nunca la menciona (a diferencia de la excepción de leash, documentada en ambos lados). Igual para el carve-out de expiración de buff (F-RB4) fuera de `apply_damage`. +- **F2-W4 — `game_time_paused` nunca llega al Temporizador (2B-W2, OQ-P4 abierta).** Impacto estrecho (el reloj de acercamiento ya está congelado en CLIMAX) pero es una brecha auto-reconocida. +- **F2-W5 — `camera_pan_speed`/`zoom_min`/`zoom_max`: definidos como knobs en `input.md`, pero el registro + 4 docs dicen que Datos de Era los posee — y Datos de Era no define ninguno (2A-D1).** El rango `zoom_max` de Input (→2.0) excede el dominio [0.5,1.5] documentado de las fórmulas de transformación de UI-HUD/Control. +- **F2-W6 — Indicador de guardado de UI/HUD huérfano (2A-A3, 2B-W5).** Guardado presupuesta un knob de indicador `SAVING`, pero `ui-hud.md` nunca referencia `SAVING`/"guardado" (0 hits). AC-07 de Guardado tiene blocker stale. +- **F2-W7 — `invocation_charges`/`draw_size`/`pool_cap`: knob global vs override por-era sin resolver (2A-D5, OQ-RB4).** Declarados como knobs de Reliquias Y candidatos a campos de `EraDefinition`; Datos de Era no los tiene en su schema ni conoce a Reliquias. +- **F2-W8 — Kaiju `attack_damage`/`attack_cooldown_s`: tuneados en Combate, restringidos en Kaiju, invisible desde el lado de tuning (2A-D6, OQ-10 abierta).** El loud-fail al cargar `EraDefinition` (AC-K74) atrapa violaciones, pero la tabla de tuning de Combate no tiene puntero de vuelta. +- **F2-W9 — Deriva menor de wiring:** Héroes coloca 2 sistemas upstream (Temporizador, Encuentro) en su tabla de *dependientes* (2A-A10); AC-H23 cita el rango superseded `0.05–0.15` (correcto: `0.10–0.15` desde 2026-08-17) (2A); filas de dependiente faltantes de bajo impacto: Datos-de-Era←Control (A8), Guardado←Tropas (A9), Datos-de-Era←{Combate (A4), UI-HUD (A5)}; aristas rotas Input→{Reliquias, UI-HUD, Permadeath} (A6, conflicto de dirección con Permadeath). + +### ✅ Verificado limpio (Fase 2) + +Ownership de la ruta de muerte (Héroes→Permadeath→Forja→Guardado) totalmente reconciliada; Reliquias R10 vs Forja R5 ortogonales (sin contradicción — Forja forja siempre, Reliquias decide *cuándo* es invocable); Furia-(b)-vs-sacrificio **sin contradicción a nivel de regla** (el umbral se recomputa hacia abajo, intencional — el problema es estrategia emergente, ver Fase 4 S7, no inconsistencia); `receive_relic`, cortes de tier `[0.10,0.35)/[0.35,0.70)/[0.70,1.00]`, `base_accidental`=0.10, campos de `RelicRecord`, y ~23 de ~25 símbolos cross-GDD resuelven a un dueño con semántica coincidente. La disciplina anti-duplicación (secciones "knobs que no existen" en Forja/Permadeath/Reliquias) es real y generalizada — F2-B/W son las excepciones, no el patrón. Cadenas de fórmula limpias verificadas: daño/telegrafío de kaiju → histéresis de UI (F1); origen del reloj de Temporizador vs reloj de enrage de Kaiju (secuenciales, sin conflicto); resolución de tick de dos fases de Combate ↔ modelo de expiración F-RB4. + +*Nota de fórmula (2B-C1, no es contradicción cross-doc): la banda Tier 1 "Delgada" `[0.10,0.35)` es alcanzable en un solo punto (`0.10` exacto) — el conjunto real de H4 es `{0.10} ∪ [0.40,1.00]`, auto-documentado en FL1. Probablemente intencional (muertes accidentales planas), confirmar antes de que UI/telemetría asuma varianza en Delgada.* + +## Cross-System Scenario Issues (Fase 4 — COMPLETA, re-corrida 2026-08-18 sesión limpia) + +7 escenarios recorridos (5 pre-identificados + 2 añadidos), agente game-designer, sin truncar. Cada hallazgo cita sistema, paso exacto de falla y la regla/AC que lo cubre o no. + +**Escenarios:** S1 death→forge→pool en CLIMAX bajo Regla 10 · S2 `offering_time_scale` × telegrafío del kaiju · S3 expiración de buff defensivo en HP crítico · S4 autosave/reload con estado efímero de invocación · S5 rally en tope de banda · S6 muerte durante un `OFFERING` abierto (añadido) · S7 Furia disparador (b) relajado por la muerte que otorga reliquia (añadido). + +### 🔴 Blockers + +**S1 — La Regla 10 (el pool excluye muertes intra-era) no tiene mecanismo implementable.** El contrato de lectura declarado de Reliquias (Regla 1) **omite `origin_era_id`** de los campos que consume del `RelicRecord`, y F-RB5 (`active_pool_size = min(total_relics_forged, pool_cap)`) nunca filtra por era. La regla que resolvió el B1 de la corrida previa es inimplementable como está escrita. Alcanzable cada vez que un héroe muere en CLIMAX. Sin señal de HUD de que la reliquia recién forjada está inerte esta era (→ Warning S1). + +**S2 — Ownership/mecanismo de `offering_time_scale` es un Open Question (OQ-RB9) dentro de un GDD Approved.** Ningún doc dice si el reloj de cooldown/tick de daño de Combate lo respeta; la tabla de Interactions de Combate solo nombra `game_time_paused`. Dos implementaciones divergentes sin adjudicar (global `Engine.time_scale` ralentiza el reloj de Furia de Kaiju; multiplicador solo-unidades hace que tus tropas hiper-ataquen a un kaiju al ralentí). **Loop de stalling no acotado:** abrir la oferta es gratis (commit-on-choice), así abrir→cancelar→cooldown 3s→reabrir da cámara lenta 0.2× casi continua toda la CLIMAX; si ralentiza `climax_elapsed_s` también estanca el temporizador anti-turtle de Furia (a). Ni Reliquias ni Kaiju lo tratan en Edge Cases. + +**S4 — La tabla de Interactions Approved de Guardado omite Reliquias por completo.** Persistencia de cargas gastadas, modificadores activos y estado de `OFFERING` marcados como "contrato a coordinar" (sin resolver). Alcanzable en cualquier crash/quit mid-CLIMAX tras gastar una carga: el default (hasta que un implementador lo note) **refunda la carga**, revirtiendo la decisión anti-abuso explícita ("cargas gastadas NO se devuelven"). **Segundo blocker:** `expiry_tick` de F-RB4 es relativo a un contador de tick de física que resetea en recarga, **sin contrato de rebasing** → el modificador resucita como permanente, o expira inmediatamente. + +**S5 — `band_headroom` / tope total de ejército indefinido.** Concepto load-bearing para una mecánica Approved (`rally`, RU-3b) que no existe en Tropas ni Datos de Era (grep: cero). Ver F2-B5. + +**S6 — La precedencia pausa-Permadeath-vs-slow-mo-`OFFERING` es un Open Question, no una regla ratificada.** OQ-RB9 la esboza pero dice que "bloquea la implementación". Permadeath Regla 4 (dueño canónico de `game_time_paused`) lista solo Combate y Kaiju — Reliquias ausente. **Segundo blocker:** ningún doc especifica el mecanismo de máquina de estados para "preservar" la oferta durante la pausa (¿`offering_soft_timeout_s` sondea `game_time_paused`? ¿`offering_time_scale` se sobreescribe a 0?). **Tercer blocker (compuesto):** si el objetivo de la bendición en vuelo es el héroe que murió, la resolución difiere a un *segundo* OQ (OQ-RB7) — sobre un mecanismo de selección de objetivo de bendición single-hero que Reliquias nunca especifica. + +**S7 — Alcance de congelamiento ambiguo para la ventana de desenganche de Furia durante `DEATH_HOLD`.** Kaiju Regla 11 enumera los relojes congelados durante `game_time_paused` pero **omite `hero_disengage_window_s`** (F5), y Furia se describe como "ortogonal" al ciclo de intención que Regla 11 congela. Si la ventana deslizante sigue avanzando durante un beat multi-muerte comprimido (Permadeath Regla 8), la cadena podría caminar el roster hacia un disparo involuntario de Furia con cero agencia del jugador — lo opuesto a lo que una mecánica anti-turtle "el jugador eligió desenganchar" debe castigar. + +### ⚠️ Warnings + +- **S1** — Sin señal de UI de que una reliquia recién forjada está inerte esta era (el contrato RU-1..RU-4 solo expone cargas/oferta/activas). +- **S5** — "Carga gastada para efecto cero" sigue siendo riesgo real incluso una vez definido `band_headroom`, porque la única salvaguarda es un pre-aviso de UI (RU-3b) gateado en una cantidad indefinida, y el recurso desperdiciado carga peso emocional (Pilar 1/2). +- **S7 — Estrategia dominante emergente (cara de escenario del B3 de la corrida previa, aún solo trackeada como OQ-15 sin aplicar):** sacrificar deliberadamente un héroe desenganchado (idealmente con `D=1`, que además maximiza `relic_quality`) para relajar `required_engaged_heroes` del roster sobreviviente y comprar espacio de turtle — el loophole exacto que el comentario de Kaiju Regla 10b intenta prevenir, no flaggeado como tradeoff aceptado. + +### ℹ️ Info + +- **S1** — Mismatch de estado de doc: Combate Edge Cases aún describen el contrato `game_time_paused` como "a confirmar", mientras Permadeath Regla 4 lo trata como satisfecho vía AC-C52 (convergen en la misma conducta). +- **S3 — Expiración de buff defensivo "nunca mata" está formula-verificada verdadera** (`min(current_health, new_max_health)` con `base_stat ≥ 1` no puede producir ≤0). El `time_to_death` de Kaiju (F2, snapshot en telegrafío) puede quedar stale tras un clamp de expiración mid-telegraph, pero el readout visible (`danger_time_s`, UI-HUD F1) se auto-corrige vía su término de HP en vivo — la garantía "nunca emboscar" se sostiene, aunque esa compensación nunca se declara intencional. +- **S3/S4** — El clamp de expiración (F-RB4) bypassa la autoridad única de `apply_damage` de Combate Regla 1 — excepción deliberada del lado de Reliquias, no cross-reconocida en Combate (ver F2-W3). +- **S4** — Perder un `OFFERING` en progreso (draw mostrado, sin pick) en crash/reload es baja severidad (ninguna carga gastada aún) pero no descrito como conducta esperada. + +### Brechas adyacentes no expandidas (para una futura pasada) +(a) Si el targeting de bendición single-hero ofensiva/defensiva es elegido por el jugador o auto-seleccionado (surgió en S6). (b) Interacción entre el freeze de Kaiju Regla 11 y el contrato de pausa de cooldown de Combate cuando un beat `DEATH_HOLD` y una ventana `STAGGERED` están activos simultáneamente (adyacente a S1). + +--- + +## GDDs Flagged for Revision (consolidado Fase 2 + 3 + 4) + +| GDD | Razón (id de hallazgo) | Tipo | Prioridad | +|-----|--------|------|----------| +| reliquias-bendiciones.md | F2-B1 (effective_attack_damage), F2-B3 (5 filas de dep faltan), F2-B4 (arista Permadeath + owner de offering_time_scale), F2-B5 (rally/band cap), S1 (mecanismo Regla 10), S4 (persistencia), S6 (muerte durante OFFERING + targeting); Fase 3 B1/B4/W1/W6 | Consistency+Design | Blocking | +| combate-dano.md | F2-B1 (capa de instancia / hook effective_attack_damage), F2-W3 (silente sobre Furia + carve-out de expiración), F2-B7 (residuo "sin GDD") | Consistency | Blocking | +| guardado-persistencia.md | S4 (tabla de Interactions omite Reliquias; contrato de rebasing de tick), F2-W6 (AC-07 stale) | Consistency | Blocking | +| forja-de-legado.md | F2-B2 (conflicto de superficie de API get_relic_combat_inputs/list_relics), F2-B7 (residuo #12) | Consistency | Blocking | +| encuentro-con-kaiju.md | S7 (alcance de freeze de ventana de Furia), F2-W1 (ancla defensiva bajo Furia), F2-W2 (kaiju_max_health), S7-W (loophole de sacrificio = OQ-15); Fase 3 B3 | Design+Consistency | Blocking | +| permadeath.md | F2-B4 (Regla 4 debe listar Reliquias), F2-W4 (game_time_paused→Temporizador, OQ-P4), F2-B7 (etiquetas stale); Fase 3 W6 | Consistency | Blocking | +| systems-index.md | F2-B6 (Dependency Map contradice tabla de enumeración; "sin circulares" no soportado; estado de Permadeath) | Consistency | Blocking | +| game-concept.md | Fase 3 B1/B5 (RESUELTOS en adenda — mantener flag hasta re-verificar) | Design | Resuelto/verificar | +| sistema-de-tropas.md | F2-B5 (tope de banda/rally), F2-B3 (fila de dep de Reliquias), F2-B7 (residuo) | Consistency | Warning | +| datos-de-era-civilizacion.md | F2-W5 (ownership de knobs de cámara/zoom), F2-W7 (cargas por-era), F2-W9 (filas de dependiente faltantes) | Consistency | Warning | +| ui-hud.md | F2-W6 (indicador de guardado huérfano), F2-W9 (ruteo de active_input_method), F2-B7 (etiquetas stale); Fase 3 B2 (overload OQ-U8 → /ux-design) | Consistency | Warning | +| input.md | F2-W5 (knobs de cámara/zoom vs dueño del registro), F2-W9 (3 aristas downstream rotas) | Consistency | Warning | +| sistema-de-heroes.md | F2-W9 (2 sistemas upstream en tabla de dependientes; rango stale 0.05–0.15 en AC-H23; residuo) | Consistency | Warning | + +--- + +## Verdict: FAIL + +**Ahora con los 3 pases completos** (Fase 2 consistencia + Fase 3 holismo + Fase 4 escenarios). ~8 clústeres de causa raíz bloqueantes; la mayoría traza a que `reliquias-bendiciones.md` (#12) fue cableada al corpus *después* de que sus vecinos se congelaron, y su hook de daño nunca se aterrizó en Combate. + +**Dos son decisiones de arquitectura, no limpieza de docs** — requieren ADR antes de `/create-architecture`: +1. **F2-B1** — quién posee la capa de modificador de instancia que `effective_attack_damage` necesita (Combate solo-por-tipo vs Reliquias). Detectado por los dos agentes de consistencia independientemente y confirmado por Fase 4. +2. **F2-B4 / S2** — quién posee y aplica el reloj `offering_time_scale`, y su interacción con `game_time_paused` de Permadeath y el tick fijo de física. + +**El resto es reconciliación de Reliquias que nunca corrió tras aprobar #12**: 5 filas de dep recíprocas (F2-B3), tabla de Guardado (S4), mecanismo de Regla 10 (S1), tope de banda para rally (F2-B5), muerte durante OFFERING (S6), y el batch de residuo "sin GDD" que gatea ACs (F2-B7). Más el auto-contradictorio Dependency Map del índice (F2-B6) y el alcance de freeze de Furia (S7). + +**No avanzar a `/create-architecture`.** Orden sugerido: (1) triar F2-B1 y F2-B4/S2 con el usuario → ADRs; (2) reconciliación de wiring de Reliquias (F2-B3/B7, S4, S6) — mecánico, alto volumen, mejor en su propia sesión; (3) F2-B5/S5, S1, S7 — decisiones de diseño acotadas; (4) re-verificar B1/B5 de Fase 3 (adenda) siguen sostenidos. Fase 3 no se re-corrió esta pasada (su holismo sigue vigente + adenda de resolución de la misma fecha). + +--- + +## Adenda de Resolución (2026-08-18, misma sesión — decisiones del usuario aplicadas) + +- **B1 — RESUELTO (reencuadre de hipótesis MVP).** `game-concept.md` MVP Definition reescrita: el MVP valida la *producción* de reliquias (permadeath→forja de muertes presenciadas, in-era) y el *feel de invocación* (con panteón pre-sembrado) **por separado**; la unión cross-era ("usar tu propia muerte presenciada") se difiere al Vertical Slice. Coherente con la Regla 10 de Reliquias y con el onboarding del concepto. Contradicción cerrada. +- **B5 — RESUELTO (reencuadre del lenguaje del hook).** `game-concept.md` Unique Hook + Core Mechanic #2 reescritos: el poder refleja "quién fue y cómo cayó" (rol → tipo de bendición; calidad de la muerte → potencia), no "exactamente cómo murió". Nota de fidelidad Pilar 2 añadida. Se acepta el split categoría-autorada / tier-derivado como diseño. +- **B2 — PARCIAL (fix concreto aplicado; overload de fondo diferido).** `ui-hud.md` reconciliado: filas de dependencia de Reliquias des-stale-adas (era "sin GDD"), AC-U30 desbloqueada, OQ-U4 resuelta. OQ-U8 (overload del HUD de CLIMAX) explícitamente marcada para **re-contar el draft** como stream activo → queda para `/ux-design` del overlay de invocación (ya flageado en el UX Flag de Reliquias). +- **B3/B4 — PARCIAL (reencuadre aplicado; guard de Furia propuesto, no mutado).** Retórica "finitas/no farmeables" reencuadrada en `reliquias-bendiciones.md` (panteón = colección que crece; lo escaso son las cargas/era) y en `game-concept.md`. Interacción farmeo × Furia(b) documentada como **OQ-15 en `encuentro-con-kaiju.md`** con guard preciso propuesto (una muerte D=1 no relaja `required_engaged_heroes`) — **no aplicado a la fórmula F5** para no regresionar AC-K63c/d/e sin la validación de escenario (Fase 4). Cotas existentes documentadas: disparador (a) time-cap roster-independiente + gating de `relic_quality`. +- **W6 — RESUELTO (contrato de precedencia).** `reliquias-bendiciones.md` OQ-RB9 ampliada: si un héroe muere durante `OFFERING`, la pausa total de Permadeath (`game_time_paused`) tiene precedencia sobre `offering_time_scale`; la oferta se preserva y reanuda. Permadeath añadido a la lista de coordinación. + +**Estado post-adenda:** B1/B5/W6 cerrados; B2/B3 con fix parcial + deferral explícito; **Fase 2 (consistencia completa) y Fase 4 (escenarios) siguen pendientes de re-correr en sesión limpia**. El veredicto FAIL se mantiene hasta ese re-run — no avanzar a `/create-architecture` antes. diff --git a/design/gdd/guardado-persistencia.md b/design/gdd/guardado-persistencia.md new file mode 100644 index 0000000000..6cbf4b3386 --- /dev/null +++ b/design/gdd/guardado-persistencia.md @@ -0,0 +1,271 @@ +# Guardado/Persistencia + +> **Status**: In Design +> **Author**: usuario + agentes +> **Last Updated**: 2026-08-15 — corregido el dueño del trigger de autosave de muerte de héroe: Sistema de Héroes (`DYING`, AC-H26), no Permadeath (Rule 1, Interactions, Dependencies, AC-01/AC-01b, nota de diseño de la Regla `retry_delay_ms`) — cierra `design/gdd/permadeath.md` OQ-P6 #3. Previo: 2026-08-09 — añadido contrato de persistencia con Temporizador de Preparación/Ritual (`elapsed_s`/`current_phase`); corregido Rule 2/AC-05 (el reloj corre desde el instante ACTIVE, sin ventana de Preparación previa) +> **Implements Pillar**: Sacrificio con Peso (Pilar 1); El Pasado es Poder (Pilar 2) + +## Overview + +El sistema de Guardado/Persistencia es la capa de infraestructura que escribe y lee el estado mutable del juego a disco: qué héroes murieron y cómo, qué reliquias se forjaron, qué era está activa, y qué progreso del panteón se ha acumulado. No contiene lógica de juego — expone una API de lectura/escritura que otros sistemas (Datos de Era/Civilización, Forja de Legado, Salón Conmemorativo/Panteón, Transición de Era) consultan para persistir sus decisiones entre sesiones. Su diseño existe en función directa del Pilar 1 (Sacrificio con Peso): si el estado de un héroe muerto pudiera revertirse recargando una partida guardada de un punto anterior a su muerte, el juego rompería su promesa central. Este sistema es, en un sentido literal, el que hace que las consecuencias sean permanentes. + +## Player Fantasy + +Este sistema no tiene fantasía propia — es la infraestructura silenciosa que hace posible una fantasía ajena: la de que las decisiones importan. El jugador nunca "interactúa" con el guardado; lo experimenta únicamente en su ausencia — la ausencia de un botón de "cargar partida anterior a la muerte", la ausencia de la posibilidad de deshacer un sacrificio. Esa ausencia es intencional y es, en sí misma, la manifestación mecánica del Pilar 1 (Sacrificio con Peso): un sistema de guardado que permitiera revertir una muerte de héroe no sería un bug, sería una traición al contrato emocional del juego. Su segunda responsabilidad silenciosa, ligada al Pilar 2 (El Pasado es Poder), es asegurar que el panteón de reliquias del jugador sobreviva entre sesiones — que cerrar el juego nunca signifique perder la historia acumulada. + +## Detailed Design + +### Core Rules + +1. **Autosave obligatorio e inmediato en eventos de consecuencia**: la muerte de un héroe (disparado por Sistema de Héroes al entrar a `DYING` — no por Permadeath, ver Interactions), la forja de una reliquia (Forja de Legado), y el fin de una era (Transición de Era) disparan un autosave automático y no configurable. La escritura debe completarse antes de que el juego continúe a la siguiente pantalla/estado, para que un crash o cierre forzado inmediatamente después del evento no pueda revertirlo. +2. **Guardado manual solo en puntos de pausa segura**: el jugador puede guardar manualmente durante las fases `PREPARATION`/`IMMINENT` del Temporizador de Preparación/Ritual, o en el Salón Conmemorativo. (El temporizador de acercamiento del kaiju corre desde el instante en que la era pasa a `ACTIVE` — no existe una ventana de Preparación previa sin reloj corriendo, ver `design/gdd/temporizador-de-preparacion-ritual.md` Regla 2.) Nunca está disponible durante `CLIMAX` (Combate) ni durante el beat sostenido `DEATH_HOLD` (coincide con el edge case ya fijado en el GDD de Input, que suprime todo input durante ese beat). +3. **Un solo slot de guardado por legado**: cada partida sobrescribe siempre el mismo archivo. No existen "guardados paralelos" — coincide con el diseño de panteón acumulativo y personal. +4. **Persistencia por referencia, nunca por copia**: el archivo de guardado almacena únicamente estado mutable (qué `era_id` está activa/archivada, qué IDs de reliquia se forjaron, contadores de progreso del panteón, `elapsed_s`/`current_phase` del Temporizador de Preparación/Ritual de la era activa) — nunca copia los datos inmutables de `EraDefinition` (coincide con la regla ya fijada en Datos de Era/Civilización). +5. **Escritura atómica**: se escribe primero a un archivo temporal (`save.tmp`), y solo se reemplaza el archivo final tras confirmar que la escritura fue exitosa (rename atómico) — previene corrupción si el proceso se interrumpe a mitad de escritura. +6. **Versionado de esquema**: cada guardado almacena un campo `schema_version: int`, comparado contra una constante `CURRENT_SCHEMA_VERSION` (inicia en 1, incrementa en 1 con cada cambio de esquema que rompa compatibilidad). Si `save.schema_version < CURRENT_SCHEMA_VERSION`, se ejecuta una cadena lineal de migraciones, cada una responsable solo de `v → v+1` (nunca salta versiones). Si `save.schema_version > CURRENT_SCHEMA_VERSION` (el jugador volvió a una build más antigua), no se intenta downgrade — se bloquea con un mensaje explícito, nunca se adivina silenciosamente una migración hacia atrás. +7. **Checksum de corrupción**: al escribir, se calcula un checksum sobre el payload serializado y se almacena junto a los datos. Al cargar, se recalcula el checksum y se compara contra el almacenado. Si no coinciden, el estado transiciona a `CORRUPTED` — sin auto-reparación, sin ignorar silenciosamente el fallo. + +### States and Transitions + +| Estado | Descripción | Transición desde | Transición hacia | +|---|---|---|---| +| `NO_SAVE_EXISTS` | No hay archivo de guardado — partida nueva | — | `SAVING` (al primer autosave) | +| `SAVING` | Escritura atómica en progreso (breve) | `NO_SAVE_EXISTS`, `SAVE_EXISTS` | `SAVE_EXISTS` (éxito), `CORRUPTED` (fallo de escritura) | +| `SAVE_EXISTS` | Guardado válido en disco, cargable | `SAVING` | `SAVING` (siguiente autosave/guardado manual) | +| `CORRUPTED` | El archivo existe pero falla la validación/lectura | `SAVING` (fallo) | — (ver Edge Cases) | + +### Interactions with Other Systems + +| Sistema | Dirección | Interfaz | +|---|---|---| +| Sistema de Héroes | consumidor → escritura | Dispara autosave obligatorio inmediatamente al transicionar un héroe a `DYING` (AC-H26) — no es Permadeath quien dispara este autosave, aunque ocurre antes de que Permadeath reciba el payload o el beat `DEATH_HOLD` comience (corregido 2026-08-15). El snapshot persistido debe incluir el payload de muerte completo (`hero_id`, `hero_name`, `relic_category`, `relic_quality` ya computada, D/T/P) marcado como *muerte pendiente de sellar* — no solo `state=DYING` — para que el flujo de carga pueda coaccionar `DYING→DEAD` y forjar la reliquia si el juego crashea a mitad del beat sostenido de Permadeath (contrato con `design/gdd/permadeath.md` Regla 6/8b, `design/gdd/sistema-de-heroes.md` AC-H26/AC-H27c) | +| Forja de Legado | consumidor → escritura | Dispara autosave tras crear una nueva reliquia | +| Transición de Era | bidireccional | Dispara autosave al completar una era; lee/escribe qué `era_id` está `ACTIVE`/`ARCHIVED` | +| Datos de Era/Civilización | lectura únicamente | Lee referencias inmutables (`era_id`) para saber qué `EraDefinition` cargar; nunca escribe en `EraDefinition` | +| Salón Conmemorativo/Panteón | lectura + guardado manual | Lee el estado persistido para mostrar el panteón acumulado; es uno de los puntos de pausa segura para guardado manual | +| UI/HUD | consumidor | Puede mostrar un indicador breve de "guardando..." durante el estado `SAVING` (a definir en detalle en UI Requirements) | +| Temporizador de Preparación/Ritual | bidireccional | Persiste `elapsed_s` y `current_phase` como parte del estado guardable; en autosave o guardado manual durante `PREPARATION`/`IMMINENT` se capturan en ese frame; al recargar, el Temporizador reanuda desde `elapsed_s` y recomputa la fase vía su Fórmula F3 (no confía en el valor de fase persistido) — contrato confirmado en `design/gdd/temporizador-de-preparacion-ritual.md` | + +*(Nota provisional: Forja de Legado, Transición de Era, Salón Conmemorativo/Panteón y UI/HUD aún no tienen GDD — estas interfaces son contratos propuestos. Sistema de Héroes y Permadeath ya tienen GDD [`design/gdd/sistema-de-heroes.md`, `design/gdd/permadeath.md`] — el trigger del autosave de muerte de héroe es Sistema de Héroes en `DYING`, no Permadeath, corregido 2026-08-15.)* + +## Formulas + +Este sistema tiene una única fórmula real — el backoff de reintento de escritura fallida. El resto de valores de timing (ej. umbral de indicador de UI) son constantes/tuning knobs sin relación computada, no fórmulas, y viven en sus secciones correspondientes. + +### `retry_delay_ms` (backoff de reintento de escritura) + +`retry_delay_ms(attempt) = base_delay_ms × backoff_multiplier^(attempt − 1)` + +| Variable | Símbolo | Tipo | Rango | Descripción | +|---|---|---|---|---| +| Intento | `attempt` | int | 1–max_retries | Qué número de reintento es (1 = primer reintento tras la escritura inicial fallida) | +| Retraso base | `base_delay_ms` | int (constante) | 50–200 | Tiempo de espera antes del primer reintento | +| Multiplicador de backoff | `backoff_multiplier` | float (constante) | 1.5–3.0 | Tasa de crecimiento del retraso por intento sucesivo | +| Reintentos máximos | `max_retries` | int (constante) | 2–5 | Total de intentos permitidos antes de exponer el fallo al jugador | +| **Retraso de reintento** | `retry_delay_ms` | int (salida) | ms | Tiempo de espera antes del intento dado | + +**Rango de salida**: acotado por la suma de todos los retrasos de reintento — una ventana deliberadamente corta, ya que los reintentos solo ayudan con fallos transitorios (disco lleno o permisos no se resuelven esperando, así que escalar indefinidamente solo estancaría el juego sin beneficio). + +**Ejemplo trabajado** (defaults: `base_delay_ms` = 100, `backoff_multiplier` = 2.0, `max_retries` = 3): + +| attempt | retry_delay_ms | +|---|---| +| 1 | 100 | +| 2 | 200 | +| 3 | 400 | + +Estancamiento máximo total antes de exponer el fallo ≈ **700ms**. Tras el 3er reintento fallido, el juego debe **detenerse y mostrar un estado de fallo bloqueante al jugador** en vez de continuar a la siguiente pantalla — esto aplica la Regla Núcleo 1 ("debe completarse antes de que el juego continúe") también al camino de fallo, no solo al de éxito. + +**Nota de diseño (confirmada contra Permadeath, `design/gdd/permadeath.md`)**: la escritura de guardado no necesita un "timeout" propio — dado que el payload es solo un puñado de IDs y contadores por referencia (Regla Núcleo 4) más el payload de circunstancia de muerte completo (Regla Núcleo 1 actualizada), una escritura bloqueante de `FileAccess` es de duración sub-milisegundo a unos pocos ms. El autosave dispara al entrar a `DYING` (Sistema de Héroes AC-H26) — *antes* de que Permadeath reciba el payload y *antes de liberar* el beat `DEATH_HOLD` (ver Interactions) — así que la secuencia ya garantiza corrección independientemente de cuánto dure ninguno de los dos; no hace falta una fórmula de acoplamiento entre duración de guardado y duración de `DEATH_HOLD`. La duración mínima de `DEATH_HOLD` la fija Permadeath (`death_hold_min_duration_s`, MVP 2.0s) por sensación/ritmo, no por esta restricción técnica. + +## Edge Cases + +- **Si la escritura al archivo temporal falla** (disco lleno, permisos): tras agotar los reintentos de `retry_delay_ms`, el guardado anterior válido permanece completamente intacto y cargable (nunca se tocó). La respuesta correcta es "este evento específico no se pudo persistir, tu guardado previo está a salvo" — **no** transiciona a `CORRUPTED`. +- **Si el rename atómico en sí falla a mitad de operación** (raro, casi garantizado atómico a nivel de OS, pero no imposible en algunos filesystems): este es el único caso de escritura que amerita `CORRUPTED`, ya que puede dejar un estado ambiguo en disco. +- **Si `save.schema_version > CURRENT_SCHEMA_VERSION`** (el jugador volvió a una build más antigua tras jugar una más nueva): no se intenta downgrade. Se bloquea la carga con un mensaje explícito y no se ofrece "continuar de todos modos" — adivinar una migración hacia atrás arriesga corromper el panteón, violando el Pilar 2. +- **Si el checksum no coincide al cargar**: transiciona a `CORRUPTED` sin auto-reparación ni opción de "ignorar y continuar". +- **Si se intenta un guardado manual durante un estado no permitido** (Clímax de Combate, `DEATH_HOLD`): la UI no debe ni exponer la opción en esos estados; si la señal se dispara de todos modos (bug/edge), el sistema la ignora silenciosamente sin error — a diferencia de un autosave fallido, esto no representa pérdida de una decisión de consecuencia. +- **Si ocurre un crash o corte de energía exactamente durante `SAVING`** (antes de completar el rename): al reiniciar, se detecta cualquier `save.tmp` huérfano y se descarta sin usarlo — se carga el `save` final anterior, que sigue siendo válido gracias al patrón de escritura atómica. +- **Si dos eventos de consecuencia ocurren en rápida sucesión** (ej. la muerte de un héroe completa la era en el mismo frame): los autosaves se serializan, nunca se ejecutan en paralelo — el segundo evento se encola hasta que el primer `SAVING` complete. + +## Dependencies + +Este sistema no depende de ningún otro (capa Foundation). + +Sistemas que dependen de este (todas duras): + +| Sistema | Tipo | Interfaz | +|---|---|---| +| Sistema de Héroes | Dura | Dispara autosave obligatorio al transicionar un héroe a `DYING` (AC-H26) — el payload persistido debe incluir el contrato de muerte completo (incl. `relic_quality`) marcado como pendiente de sellar, para la coacción `DYING→DEAD` que el flujo de carga ejecuta si el juego crashea a mitad del beat de Permadeath (ver `design/gdd/permadeath.md` Regla 8b, `design/gdd/sistema-de-heroes.md` AC-H27c) | +| Forja de Legado | Dura | Dispara autosave tras crear una nueva reliquia | +| Transición de Era | Dura (bidireccional) | Dispara autosave al completar una era; lee/escribe qué `era_id` está `ACTIVE`/`ARCHIVED` | +| Datos de Era/Civilización | Dura (bidireccional) | Este sistema almacena qué `era_id` está activa/completada, referenciando sus datos inmutables sin copiarlos (relación ya documentada en `design/gdd/datos-de-era-civilizacion.md`) | +| Salón Conmemorativo/Panteón | Dura | Lee el estado persistido para mostrar el panteón acumulado; punto de pausa segura para guardado manual | +| UI/HUD | Dura | Lee el estado `SAVING` para mostrar el indicador de "guardando..." | +| Temporizador de Preparación/Ritual | Dura (bidireccional) | Este sistema almacena `elapsed_s`/`current_phase` como estado mutable de la era activa, referenciando el contrato ya documentado en `design/gdd/temporizador-de-preparacion-ritual.md` — contrato confirmado bidireccionalmente | + +*(Nota provisional: Permadeath, Forja de Legado, Transición de Era, Salón Conmemorativo/Panteón y UI/HUD aún no tienen GDD — estas interfaces son contratos propuestos, salvo las relaciones con Datos de Era/Civilización y Temporizador de Preparación/Ritual, que ya están confirmadas bidireccionalmente en ambos documentos.)* + +## Tuning Knobs + +| Knob | Símbolo | Tipo | Default | Rango seguro | Qué se rompe en extremos | +|---|---|---|---|---|---| +| Retraso base de reintento | `base_delay_ms` | int | 100 | 50–200 | <50: no da tiempo a que se libere un bloqueo transitorio de archivo (ej. antivirus); >200: alarga innecesariamente el estancamiento total sin mejorar la tasa de éxito | +| Multiplicador de backoff | `backoff_multiplier` | float | 2.0 | 1.5–3.0 | <1.5: los reintentos quedan muy juntos, poco tiempo real para que se resuelva el bloqueo; >3.0: el estancamiento total crece demasiado rápido, empeorando la percepción de "juego congelado" | +| Reintentos máximos | `max_retries` | int | 3 | 2–5 | <2: casi no da margen a fallos transitorios reales; >5: el estancamiento total (ver Formulas) se vuelve perceptible por el jugador antes de fallar visiblemente | +| Umbral de indicador de UI de guardado | `save_indicator_display_threshold_ms` | int | 200 | 150–250 | <150: el indicador "parpadea" en escrituras normales de <16ms, generando ruido visual innecesario; >250: el jugador podría percibir el juego como colgado antes de recibir cualquier feedback de que algo está pasando | + +**Interacción**: los tres primeros knobs son parámetros de la fórmula `retry_delay_ms` (ver Formulas) — cambiarlos afecta directamente el estancamiento máximo total (~700ms con los defaults) antes de que un fallo de escritura se exponga al jugador. El umbral de indicador de UI es independiente y solo controla cuándo aparece el feedback visual de "guardando...". + +**Mecanismo de enforcement**: cualquier valor configurado fuera de su rango seguro se **clampa automáticamente al límite más cercano** al cargarse — el juego nunca falla ni se bloquea por una configuración de knob inválida. + +## Visual/Audio Requirements + +Este sistema no tiene requisitos visuales o de audio propios más allá del feedback de UI descrito abajo. Un posible SFX sutil de confirmación de guardado podría considerarse en la GDD de Audio, pero no es un requisito de este sistema. + +## UI Requirements + +Este sistema contribuye tres elementos a la UI, cuyo diseño visual detallado pertenece a la futura GDD de UI/HUD (deben seguir el lenguaje diegético del art bible Sección 7): + +1. **Indicador de "guardando..."**: aparece durante el estado `SAVING` únicamente si la escritura excede `save_indicator_display_threshold_ms` (default 200ms) — evita parpadeos en escrituras normales sub-16ms. Discreto, no bloqueante. +2. **Diálogo de fallo de guardado bloqueante**: se muestra cuando se agotan los reintentos (AC-26/AC-35). Debe comunicar que este evento específico no se persistió pero el guardado previo está a salvo, y no debe permitir "continuar" hasta que el jugador reconozca el fallo. +3. **Mensaje de guardado incompatible/corrupto**: dos variantes textualmente distintas (AC-29) — "guardado de versión más nueva" (rollback de build) vs. "guardado dañado" (checksum fallido/`CORRUPTED`) — para que el jugador nunca confunda un caso con el otro. + +**📌 UX Flag — Guardado/Persistencia**: Este sistema tiene requisitos de UI. En Pre-Producción, ejecutar `/ux-design` para crear una especificación UX de estos tres elementos (indicador de guardado + los dos diálogos de fallo) antes de escribir epics. Las stories que referencien esta UI deben citar `design/ux/[pantalla].md`, no esta GDD directamente. + +## Acceptance Criteria + +*(Validado por `qa-lead`. Los criterios marcados [BLOQUEADO] no son testeables hasta que exista la GDD del sistema mencionado.)* + +### Core Rules + +**AC-01.** Given un evento de muerte de héroe disparado en la API del sistema de Guardado (llamada simulada, sin Sistema de Héroes real), When se dispara el trigger, Then comienza un autosave automáticamente sin input del jugador, y el juego no avanza a la siguiente pantalla/estado hasta que la escritura se resuelve (`SAVE_EXISTS` o un fallo bloqueante expuesto). + +**AC-01b** [antes bloqueado en GDD de Permadeath; hoy bloqueado solo en implementación — corregido 2026-08-15]: el trigger real es la transición `DYING` de Sistema de Héroes (AC-H26), no una señal de Permadeath — ese contrato ya está fijado en `design/gdd/sistema-de-heroes.md` y `design/gdd/permadeath.md`. La muerte real de un héroe disparando el autosave de punta a punta es testeable como Integration test una vez exista una era jugable implementada con Sistema de Héroes; ya no depende de diseño pendiente. + +**AC-02.** Given un evento de reliquia forjada disparado en la API (llamada simulada), When se dispara el trigger, Then el autosave comienza y completa antes de que el juego proceda más allá de la confirmación de forja. + +**AC-02b** [BLOQUEADO — pendiente GDD de Forja de Legado]. + +**AC-03.** Given un evento de era completada disparado en la API (llamada simulada), When se dispara el trigger, Then el autosave comienza y completa antes de que el juego proceda a la pantalla de la siguiente era. + +**AC-03b** [BLOQUEADO — pendiente GDD de Transición de Era]. + +**AC-04.** Given cualquier autosave de evento de consecuencia en progreso (`SAVING`), When el juego intenta proceder a la siguiente pantalla/estado, Then la transición se retiene/bloquea hasta que `SAVING` resuelve a `SAVE_EXISTS` o se expone el camino de fallo (AC-35). + +**AC-05.** Given el juego está en la fase `PREPARATION` o `IMMINENT` del Temporizador de Preparación/Ritual (el reloj de acercamiento del kaiju ya está corriendo — no existe una fase de Preparación sin temporizador activo), When el jugador dispara guardado manual, Then el guardado ejecuta normalmente `NO_SAVE_EXISTS/SAVE_EXISTS → SAVING → SAVE_EXISTS`, y el payload persistido incluye el `elapsed_s`/`current_phase` del Temporizador en ese frame. + +**AC-06.** Given el juego está en el Salón Conmemorativo, When el jugador dispara guardado manual, Then el guardado ejecuta normalmente. + +**AC-07** [BLOQUEADO — pendiente GDD de UI/HUD]: que la UI no presente la opción de guardado manual durante Clímax/`DEATH_HOLD` no es testeable hasta que exista la especificación de la superficie de interacción del menú de guardado. + +**AC-08.** Given ya existe un archivo de guardado en la ruta única, When ocurre cualquier guardado (auto o manual), Then el archivo existente se sobrescribe en el mismo lugar — no se crea un nuevo archivo en otra ruta. + +**AC-09** [BLOQUEADO — pendiente GDD de UI/HUD, advisory]: verificar que no existe una ruta de UI para iniciar un segundo slot concurrente no es testeable sin una especificación de UI. + +**AC-10.** Given una escritura de guardado completa, When se inspecciona el payload serializado, Then contiene únicamente `era_id`, IDs de reliquia forjada, contadores de progreso del panteón, y `elapsed_s`/`current_phase` del Temporizador de Preparación/Ritual — ningún campo de `EraDefinition` (nombre, descripción, valores de balance, etc.) está presente en ningún lugar del payload. + +**AC-11.** Given un guardado que referencia `era_id = X`, When el contenido de `EraDefinition` para `X` cambia en una actualización de contenido/datos y el guardado se recarga, Then el juego cargado refleja el contenido actualizado de `EraDefinition` (prueba de que el guardado sostiene una referencia viva, no una copia obsoleta). Requiere coordinación con fixtures de test de Datos de Era/Civilización (ya diseñado, no bloqueado). + +**AC-12** [BLOQUEANTE — crítico para Pilares 1/2]. Given se dispara un guardado, When comienza la escritura, Then los datos se escriben primero a `save.tmp`, y los bytes del archivo de guardado final permanecen sin modificar durante toda la duración de esa escritura. + +**AC-13** [BLOQUEANTE]. Given `save.tmp` se escribió completa y exitosamente, When se ejecuta el rename atómico, Then es la *única* operación que modifica el archivo de guardado final, y lo reemplaza completamente o no lo toca en absoluto (ningún estado parcial es observable). + +**AC-14** [BLOQUEANTE]. Given el proceso se mata después de que `save.tmp` se escribió completamente pero antes de que el rename complete, When el juego reinicia, Then el archivo de guardado final previo es idéntico byte a byte a su estado previo a la escritura y carga exitosamente. + +**AC-15** [BLOQUEANTE]. Given un guardado con `schema_version < CURRENT_SCHEMA_VERSION`, When se carga, Then las migraciones corren secuencialmente una versión a la vez (v→v+1, nunca saltando una versión) hasta que `schema_version == CURRENT_SCHEMA_VERSION`. + +**AC-16** [BLOQUEANTE]. Given un guardado 3 versiones detrás de `CURRENT_SCHEMA_VERSION`, When se carga, Then exactamente 3 migraciones corren en orden — no 1, no 2, no fuera de orden. + +**AC-17** [BLOQUEANTE]. Given un guardado con `schema_version > CURRENT_SCHEMA_VERSION`, When se intenta cargar, Then la carga se bloquea con un mensaje explícito al jugador, ninguna migración corre, y ninguna opción de "continuar de todos modos" se presenta en ningún punto del flujo. + +**AC-18** [BLOQUEANTE]. Given un guardado con `schema_version == CURRENT_SCHEMA_VERSION`, When se carga, Then cero migraciones corren y el guardado carga directamente. + +**AC-19** [BLOQUEANTE]. Given una escritura de guardado completa, When se inspecciona el archivo, Then un checksum calculado sobre el payload serializado está presente y almacenado junto a los datos. + +**AC-20** [BLOQUEANTE]. Given un guardado cuyo checksum almacenado coincide con un checksum recalculado de su payload, When se carga, Then carga normalmente sin transición a `CORRUPTED`. + +**AC-21** [BLOQUEANTE]. Given un guardado cuyo checksum almacenado NO coincide con su checksum recalculado, When se intenta cargar, Then el estado transiciona a `CORRUPTED`, ninguna escritura de auto-reparación ocurre, y ninguna opción de "ignorar y continuar" se presenta. + +### States and Transitions + +**AC-22.** Given estado == `NO_SAVE_EXISTS`, When se dispara el primer guardado (auto o manual), Then la secuencia observada es `NO_SAVE_EXISTS → SAVING → SAVE_EXISTS` en éxito. + +**AC-23.** Given estado == `SAVE_EXISTS`, When se dispara un guardado subsecuente, Then la secuencia observada es `SAVE_EXISTS → SAVING → SAVE_EXISTS` en éxito (camino de re-guardado, mismo archivo sobrescrito según AC-08). + +**AC-24.** Given estado == `SAVING`, When el paso de rename atómico falla, Then el estado transiciona `SAVING → CORRUPTED`. + +**AC-25.** Given estado == `SAVING`, When el fallo ocurre en el paso de escritura del archivo temporal (no en el rename), Then el estado **no** transiciona a `CORRUPTED` — permanece/regresa al estado válido previo (`SAVE_EXISTS` o `NO_SAVE_EXISTS`), consistente con que el guardado previo permanece intacto. + +### Edge Cases + +**AC-26** (EC1). Given existe un guardado previo válido, When la escritura de `save.tmp` falla en cada intento de reintento (ej. disco lleno simulado) hasta agotar `max_retries`, Then se expone un fallo bloqueante al jugador, el archivo de guardado previo no cambia y sigue siendo cargable, y el estado **no** es `CORRUPTED`. + +**AC-27** (EC2). Given `save.tmp` se escribió completamente, When el rename atómico en sí falla, Then el estado transiciona a `CORRUPTED`. + +**AC-28** (discriminador EC1 vs EC2 — pedido explícitamente por la advertencia del propio GDD contra confundirlos). Given dos inyecciones de fallo separadas — una en el paso de escritura temporal, otra en el paso de rename — When ambas se reproducen en la misma sesión de test, Then el fallo de escritura temporal afirma `estado != CORRUPTED AND archivo_previo_sin_cambios == true`, mientras que el fallo de rename afirma `estado == CORRUPTED`. Un test que solo verifica "la operación de guardado reportó fallo" sin comprobar cuál de estos dos estados resultó no satisface este criterio. + +**AC-29** (EC3). Given un guardado con `schema_version > CURRENT_SCHEMA_VERSION`, When se muestra el mensaje de bloqueo, Then su texto comunica explícitamente "este guardado es de una versión más nueva del juego" (o equivalente), distinto del mensaje genérico de `CORRUPTED` usado en AC-30 — el jugador no debe poder confundir "volviste a una build anterior" con "tu guardado está dañado". + +**AC-30** (EC4). Given se detecta un desajuste de checksum al cargar, When la carga fallida completa, Then ninguna operación de escritura de ningún tipo ocurre contra el archivo de guardado durante ese intento de carga (prueba de que no hay auto-reparación o auto-reescritura silenciosa). + +**AC-31** (EC5). Given el juego está en Clímax de Combate o `DEATH_HOLD`, When una señal de guardado manual se dispara directamente contra la API del sistema (sin pasar por la UI — ej. un bug o un test automatizado), Then la solicitud se descarta sin transición de estado, sin diálogo de error, y sin escritura a disco. + +**AC-32** (EC6). Given el proceso se termina abruptamente mientras estado == `SAVING` (existe un `save.tmp` huérfano, el archivo final aún no reemplazado), When el juego reinicia, Then el `save.tmp` huérfano se detecta y se descarta sin usarlo, y el guardado final previo carga exitosamente con estado == `SAVE_EXISTS`. + +**AC-33** (EC7). Given un autosave de muerte de héroe y un autosave de era completada se disparan ambos dentro del mismo frame, When ambos se encolan, Then ejecutan estrictamente en secuencia — la segunda escritura no comienza hasta que el primer `SAVING` resolvió completamente a `SAVE_EXISTS` o `CORRUPTED`. Ninguna escritura intercalada/paralela es observable. + +### Timing de Fórmula (`retry_delay_ms`) + +**AC-34.** Given tuning por defecto (`base_delay_ms=100`, `backoff_multiplier=2.0`, `max_retries=3`) y una escritura de guardado que falla en cada intento, When se miden los reintentos, Then el intento 1 ocurre ~100ms después del fallo inicial, el intento 2 ocurre ~200ms después del fallo del intento 1, y el intento 3 ocurre ~400ms después del fallo del intento 2 (± tolerancia razonable del scheduler). + +**AC-35.** Given el mismo tuning por defecto, When los 3 reintentos fallan, Then el tiempo total de estancamiento desde el fallo inicial hasta que el fallo bloqueante se expone al jugador es ~700ms (100+200+400), y el estado de fallo bloqueante aparece en ese punto — ni antes, ni indefinidamente después. + +**AC-36.** Given `max_retries=3` (default), When el 3er reintento falla, Then no se programa un 4to intento — el fallo se expone inmediatamente tras el fallo del 3er reintento. + +### Tuning Knobs — Clamping de Rango Seguro + +**AC-37.** Given `base_delay_ms` configurado por debajo de 50 (ej. 10), When el valor se carga, Then el valor efectivo usado por `retry_delay_ms` se clampa a 50. + +**AC-38.** Given `base_delay_ms` configurado por encima de 200 (ej. 500), When se carga, Then el valor efectivo se clampa a 200. + +**AC-39.** Given `backoff_multiplier` configurado por debajo de 1.5 (ej. 1.0), When se carga, Then el valor efectivo se clampa a 1.5. + +**AC-40.** Given `backoff_multiplier` configurado por encima de 3.0 (ej. 5.0), When se carga, Then el valor efectivo se clampa a 3.0. + +**AC-41.** Given `max_retries` configurado por debajo de 2 (ej. 1 o 0), When se carga, Then el valor efectivo se clampa a 2. + +**AC-42.** Given `max_retries` configurado por encima de 5 (ej. 10), When se carga, Then el valor efectivo se clampa a 5. + +**AC-43.** Given `save_indicator_display_threshold_ms` configurado por debajo de 150, When se carga, Then el valor efectivo se clampa a 150. + +**AC-44.** Given `save_indicator_display_threshold_ms` configurado por encima de 250, When se carga, Then el valor efectivo se clampa a 250. + +### Clasificación de tipo de story (para el gate de evidencia) + +| AC # | Tipo | Evidencia | Gate | +|---|---|---|---| +| AC-01 a AC-06, AC-08, AC-10, AC-12 a AC-25, AC-30 a AC-42 | Logic | Unit test automatizado (`tests/unit/save-persistence/`) | Bloqueante | +| AC-11 | Integration | Integration test con Datos de Era/Civilización | Bloqueante (no bloqueado — esa GDD ya existe) | +| AC-26 a AC-29, AC-31 a AC-33 | Logic | Unit test automatizado | Bloqueante | +| AC-34 a AC-36 | Logic | Unit test automatizado | Bloqueante | +| AC-43, AC-44 | Config/Data | Smoke check | Advisory | +| AC-07, AC-09 | Integration/UI | Bloqueado, ver huecos | Advisory | +| AC-01b, AC-02b, AC-03b | Integration | Bloqueado, ver huecos | Bloqueante una vez desbloqueado | + +### Huecos conocidos (a resolver cuando existan las GDDs dependientes) + +1. **AC-01b/02b/03b** bloqueados hasta que existan las GDDs de Permadeath, Forja de Legado y Transición de Era — el contrato propio de este sistema (escribir-antes-de-proceder, reintento/atomicidad/checksum) es completamente testeable hoy vía triggers simulados; el cableado real de punta a punta no. +2. **AC-07** bloqueado hasta la GDD de UI/HUD — la lógica de "ignorar si se dispara de todos modos" (AC-31) no depende de esto y es testeable ahora. +3. **AC-09** bloqueado hasta la GDD de UI/HUD (y posiblemente un futuro flujo de menú principal). +4. **AC-06** solo testeable a nivel de flag de estado hoy — confianza completa requiere que la GDD del Salón Conmemorativo/Panteón confirme que entrar a esa escena efectivamente activa el flag que este sistema revisa. +5. El comportamiento visual del indicador de UI (aparece/desaparece en el umbral) está bloqueado hasta la GDD de UI/HUD — solo el clamping numérico de la constante (AC-43/44) es testeable en aislamiento ahora. + +## Open Questions + +| Pregunta | Owner | Resolución objetivo | +|---|---|---| +| ¿Qué algoritmo de checksum usar (CRC32, `hash()` de Godot, u otro)? Es una decisión de implementación/rendimiento, no de diseño — no necesita ser criptográficamente seguro (defiende contra corrupción/escrituras parciales, no contra manipulación) | godot-gdscript-specialist / technical-director | Al implementar (ADR) | +| La duración mínima de `DEATH_HOLD` debe elegirse por sensación/ritmo en la GDD de Permadeath, independiente de la latencia de escritura de guardado — la secuencia (autosave-antes-de-liberar) ya garantiza corrección sin importar la duración | game-designer | Al diseñar Permadeath | +| ¿Debería registrarse `CURRENT_SCHEMA_VERSION` en el registro de constantes? Aún no lo referencia ningún otro GDD; revisitar cuando exista un doc de release/patch-notes que dependa de él | game-designer | Al establecer el proceso de release | +| Las interfaces con Permadeath, Forja de Legado, Transición de Era, Salón Conmemorativo/Panteón y UI/HUD son contratos propuestos — confirmar bidireccionalmente cuando cada uno tenga su GDD | game-designer | Al diseñar cada sistema dependiente | diff --git a/design/gdd/input.md b/design/gdd/input.md new file mode 100644 index 0000000000..1166d31f8e --- /dev/null +++ b/design/gdd/input.md @@ -0,0 +1,299 @@ +# Sistema de Input + +> **Status**: Needs Revision (design-review 2026-08-19: NEEDS REVISION → 7 bloqueantes aplicados esta sesión — disconnect×DEATH_HOLD, dirección de zoom, feedback de lockout, invariante Pilar 1 distribuida, gate AC 22, fórmulas sin especificar, bidireccionalidad. Ver design/gdd/reviews/input-review-log.md) +> **Author**: usuario + agentes +> **Last Updated**: 2026-08-19 +> **Implements Pillar**: Preparación Ritual, Clímax Explosivo (soporte); infraestructura para todos los pilares +> **Creative Director Review (CD-GDD-ALIGN)**: Omitido — Modo lean + +## Overview + +El Sistema de Input es la capa fundacional que traduce las acciones físicas del jugador (mouse, teclado, gamepad) en eventos de intención abstractos que el resto del juego consume — selección de unidades, movimiento de cámara, invocación de bendiciones, navegación de UI. No implementa ninguna lógica de juego por sí mismo: define y expone un conjunto de Input Actions con nombre (ej. `select_unit`, `command_move`, `invoke_blessing`, `pause_menu`) que se mapean a múltiples dispositivos de entrada simultáneamente, permitiendo que PC (mouse+teclado) y consola (gamepad) compartan la misma capa de intención sin que ningún otro sistema necesite conocer qué dispositivo generó la acción. Sin este sistema, ningún otro sistema del juego podría recibir input del jugador de forma consistente entre plataformas. + +## Player Fantasy + +El Sistema de Input no tiene una fantasía propia — es invisible por diseño. Su éxito se mide en que el jugador nunca piense en él: durante la Preparación, el control debe sentirse preciso y deliberado (cada clic de posicionamiento importa, según el Pilar 3); durante el Clímax, debe sentirse instantáneo y confiable, sin fricción entre la intención del jugador y la acción en pantalla. La única vez que el input "se siente" es cuando falla — y dado que el Pilar 1 (Sacrificio con Peso) exige que la muerte de un héroe sea siempre una decisión legítima del jugador, nunca un accidente técnico, este sistema tiene una responsabilidad silenciosa pero crítica en su parte del contrato: **la fidelidad de la señal cruda** — cada intención física del jugador se captura sin ambigüedad ni pérdida (zonas muertas, umbrales, ventanas de doble-clic bien afinados). La entrada del jugador debe ser tan confiable que cuando ocurra una pérdida, el jugador nunca pueda culpar al control — solo a su propia decisión. + +> **Invariante Pilar 1 distribuida (no la garantiza Input solo).** La promesa completa —"un héroe jamás muere por un input mal registrado o ambiguo"— es un **invariante cross-system con dueños nombrados**, no una garantía que este documento pueda cumplir por sí mismo. Input **solo** posee la fidelidad de la señal cruda (emite `screen_pos`/`select_requested` sin decidir qué unidad). La resolución de *cuál* unidad recibe un clic o una orden fatal la posee **Control y Selección de Unidades** (desempate héroe>tropa, `select_radius`); la evaluación de letalidad y el sellado la posee **Permadeath/Combate**. Ningún documento aislado cumple el Pilar 1: se cumple si y solo si los tres respetan sus handoffs. Dos costuras concretas quedan como contratos requeridos (ver Open Questions OQ-INPUT-1): (a) la resolución clic→unidad de Control no debe convertir un clic ambiguo en una orden letal; (b) el **orden de frame** entre Input (orden de rescate de último segundo), Control (aplicarla) y Permadeath (evaluar la muerte) debe estar especificado, para que un accidente de orden-de-actualización nunca sea indistinguible de una decisión legítima del jugador. + +## Detailed Design + +### Core Rules + +1. **Abstracción por Input Actions**: todo dispositivo físico se mapea a un conjunto fijo de acciones con nombre (`StringName`). Ningún sistema consumidor lee dispositivos crudos — solo consultan acciones abstractas. Uso de `&"action_name"` (StringName) en rutas calientes, nunca literales de string; se recomienda definirlas como `const` `StringName` en un único autoload (p. ej. `const ACTION_SELECT_UNIT := &"select_unit"`) para reducir riesgo de typo. **Excepción explícita — señales de ciclo de vida de dispositivo:** la regla "sin dispositivos crudos" prohíbe polling de HID/estado por-eje, **no** las señales de dispositivo del motor. Input **debe** conectarse a `Input.joy_connection_changed(device, connected)` de Godot para detectar conexión/desconexión de gamepad (`Input.is_action_pressed` **no** expone un evento discreto de desconexión — solo deja de reportar `true`). El identificador "gamepad activo" se rastrea por **identidad estable (GUID / `Input.get_joy_name`)**, no por índice `device`, porque el backend SDL3 de Godot 4.5+ puede reasignar índices al reconectar. *(Verificar el comportamiento exacto de reasignación de índices contra los docs de 4.6 antes de implementar — ver `docs/engine-reference/godot/`.)* +2. **Contexto de Input activo (fuente única, gating centralizado)**: existe exactamente un contexto de input activo a la vez, gestionado por una máquina de estados **dentro del propio sistema de Input**. El contexto determina qué acciones están habilitadas y cómo se enrutan. **El gating por contexto es centralizado**: los handlers `_input`/`_unhandled_input` de Input consultan `current_context` y **nunca emiten una señal de intención** para una acción no habilitada en el contexto activo — el gating no se delega a cada consumidor (un consumidor que olvide chequear su contexto no debe poder actuar). **Enforcement de `current_context` (AC 4):** dado que GDScript no tiene privacidad de campo ni en 4.6, la mutación externa directa se previene con un patrón explícito — campo privado `_current_context` accesible solo por lectura (`get`-only) y transiciones exclusivamente vía un método `request_context_transition(new) -> bool` que valida y devuelve si el cambio se aceptó. *(Este contrato cross-cutting — lo honran UI/HUD, Reliquias, Permadeath — merece un ADR; ver Open Questions.)* +3. **Detección de método de input**: el sistema rastrea el último método de input usado (mouse/teclado vs. gamepad) y expone esa señal `active_input_method`. Godot 4.6 separa el foco de mouse/touch del foco de teclado/gamepad (sistema de foco dual, confirmado contra `docs/engine-reference/godot/modules/ui.md`). **Alcance de `active_input_method` (corregido en review):** esta señal sirve para elegir *iconos de prompt* ("Presiona A" vs "Presiona Enter"), **no** para decidir la visibilidad del anillo de foco. El foco dual permite que ambos estados (mouse-hover y foco de teclado/gamepad) estén visualmente activos **simultáneamente en `Control`s distintos** — la retroalimentación de foco la maneja cada nodo vía su `focus_mode`/`has_focus()` + `mouse_entered`/`mouse_exited`, no un broadcast global de método. *(Nota para UI/HUD: verificar la API de foco de `Control` en 4.6 antes de fijar el patrón de feedback de foco — no derivarlo de `active_input_method`.)* +4. **Sin lógica de juego**: el sistema solo emite señales de intención (`unit_selected_at`, `move_commanded_to`, `blessing_invoke_requested`, etc.). No decide qué unidad se selecciona ni si un movimiento es válido — eso pertenece a los sistemas consumidores. + +### States and Transitions + +| Contexto | Acciones habilitadas | Se entra desde | Se sale hacia | +|---|---|---|---| +| `RTS_COMMAND` | select_unit, box_select, command_move, invoke_blessing, pan_camera, zoom_camera, open_pause | Inicio de era, cerrar menú, fin de BLESSING_SELECT | MENU_NAVIGATION (pausa/panteón), BLESSING_SELECT, DEATH_HOLD | +| `MENU_NAVIGATION` | ui_up, ui_down, ui_left, ui_right, ui_accept, ui_cancel | open_pause desde RTS_COMMAND, transición a Panteón | RTS_COMMAND (cerrar menú) | +| `BLESSING_SELECT` | ui_navigate (entre bendiciones ofrecidas), ui_accept (confirmar), ui_cancel (abortar) | invoke_blessing desde RTS_COMMAND | RTS_COMMAND (confirmar o cancelar) | +| `DEATH_HOLD` | ninguna (todo input de jugador suprimido) | Disparo de permadeath de héroe | RTS_COMMAND (cuando el beat sostenido se libera, controlado por el sistema de Permadeath, no por input) | + +- Solo el sistema puede cambiar de contexto; los cambios se disparan por eventos de otros sistemas (ej. Permadeath fuerza `DEATH_HOLD`), no por el jugador directamente. +- La transición a `DEATH_HOLD` es inmediata y descarta cualquier input en cola — ninguna orden emitida justo antes de una muerte "sobrevive" al beat. + +### Interactions with Other Systems + +| Sistema | Dirección | Interfaz | +|---|---|---| +| Control y Selección de Unidades | Input → consumidor | Emite `select_requested(screen_pos)`, `box_select_requested(rect)`, `move_commanded(screen_pos)`; el consumidor resuelve qué unidad/posición | +| Reliquias/Bendiciones | bidireccional | Reliquias solicita entrar a `BLESSING_SELECT`; Input emite `blessing_choice_confirmed(index)` o `blessing_selection_cancelled` | +| Permadeath | Permadeath → Input | Permadeath fuerza el contexto `DEATH_HOLD` al iniciar el beat y lo libera al terminarlo; Input no controla el timing | +| UI/HUD | Input → UI | En `MENU_NAVIGATION`/`BLESSING_SELECT`, Input emite acciones `ui_*`; también expone `active_input_method` para el feedback de foco dual (4.6) | +| Cámara (parte de Control de Unidades) | Input → consumidor | Emite `pan_requested(delta)`, `zoom_requested(delta)` solo en `RTS_COMMAND` | + +*(Actualización 2026-08-19: Control y Selección de Unidades, Reliquias/Bendiciones, Permadeath y UI/HUD **ya tienen GDD**. Las interfaces de esta tabla están confirmadas contra esos documentos; la bidireccionalidad se registró en cada uno — ver Dependencies. La costura clic→unidad y el orden de frame de la muerte quedan como contratos requeridos en OQ-INPUT-1.)* + +## Formulas + +Este sistema no tiene fórmulas de balance de juego — solo constantes de umbral de input con rangos definidos. + +### 1. Zona muerta de gamepad (con remap) +`stick_dead_zone` = magnitud radial mínima para registrar input analógico. + +| Variable | Símbolo | Tipo | Default | Rango | Qué se rompe en extremos | +|---|---|---|---|---|---| +| Zona muerta | `stick_dead_zone` | float | 0.20 | 0.10–0.30 | <0.10: deriva de cámara con pads gastados; >0.30: se pierde control fino cerca del centro | + +Zona muerta radial/circular, no por-eje, para evitar sesgo diagonal. + +**Expresión (fuente de verdad — `control-y-seleccion-de-unidades.md` Fórmula 3 consume `stick_input_dir`/`stick_input_mag` de aquí):** + +``` +raw_mag = sqrt(stick_x² + stick_y²) # magnitud cruda ∈ [0, 1] +if raw_mag ≤ stick_dead_zone: + stick_input_mag = 0.0 # dentro de zona muerta → sin input + stick_input_dir = Vector2.ZERO +else: + stick_input_mag = (raw_mag − stick_dead_zone) / (1 − stick_dead_zone) # remap [dead,1] → [0,1] + stick_input_dir = Vector2(stick_x, stick_y) / raw_mag # dirección unitaria cruda +``` + +- El remap `(raw_mag − dead) / (1 − dead)` reescala el rango útil a `[0,1]` completo, evitando un "salto" de magnitud al cruzar el umbral (sin remap, el primer input registrado saltaría de 0 a `stick_dead_zone`). +- **Inclusividad:** la comparación es `≤` (magnitud exactamente igual a `stick_dead_zone` = sin input). `stick_input_mag` resultante siempre ∈ `[0,1]`; nunca negativo (garantizado por la guarda `raw_mag > dead`). +- La dirección se toma de la magnitud cruda (antes del remap) para no distorsionar el ángulo. + +### 2. Umbral de arrastre para box-select +`drag_threshold_px` = distancia mínima en píxeles (a 1080p de referencia) antes de que un clic-sostenido se resuelva como arrastre. + +| Variable | Símbolo | Tipo | Default | Rango | Qué se rompe en extremos | +|---|---|---|---|---|---| +| Umbral de arrastre | `drag_threshold_px` | int | 5 | 3–8 | <3: temblor de mano causa box-selects accidentales; >8: arrastres rápidos fallan como clic | + +**Expresión de escalado por resolución/DPI (obligatoria, no hand-wave):** el valor de la tabla es a **1080p de referencia**; el umbral efectivo escala con la altura del viewport: + +``` +drag_threshold_px_effective = drag_threshold_px × (viewport_height_px / 1080.0) +``` + +- Motivo: sin escalar, un mismo movimiento físico de mano produce ~2× los deltas de píxel a 4K (2160p) que a 1080p, así que el default de 5px se comportaría como ~2.5px efectivos a 4K sin compensar — cayendo en el modo de falla "<3: temblor causa box-selects accidentales" que la tabla misma marca. El escalado mantiene el perdón *físico* constante entre resoluciones. +- El clamp del rango seguro (3–8) se aplica al valor **de referencia** (el knob), no al efectivo; el efectivo es una función derivada de la resolución activa. +- Este knob también alimenta el sistema de **Accesibilidad** (ver Tuning Knobs): el temblor de mano es explícitamente su modo de falla, por lo que la accesibilidad puede necesitar un techo de referencia mayor que el rango de tuning del jugador mediano (a coordinar con el sistema de Accesibilidad, igual que `double_click_window_s`). + +### 3. Ventana de doble-clic +`double_click_window_s` = segundos máximos entre dos clics para registrar doble-clic (ej. "seleccionar todas las unidades del mismo tipo"). + +| Variable | Símbolo | Tipo | Default | Rango | Qué se rompe en extremos | +|---|---|---|---|---|---| +| Ventana doble-clic | `double_click_window_s` | float | 0.3 | 0.2–0.5 | <0.2: físicamente difícil de acertar; >0.5: dos clics deliberados separados se fusionan por error | + +Implementar con timer manual comparando `Time.get_ticks_msec()` entre eventos `InputEventMouseButton`, no detección de doble-clic a nivel de OS (inconsistente entre plataformas). + +### 4. Velocidad de paneo de cámara +`camera_pan_speed` = velocidad base de traslación en **unidades-de-mundo/segundo** (no píxeles/segundo, para mantener consistencia espacial entre niveles de zoom). + +| Variable | Símbolo | Tipo | Default | Rango | Qué se rompe en extremos | +|---|---|---|---|---|---| +| Velocidad de paneo | `camera_pan_speed` | float | **30** | 27–33 | world units/sec | <27: se siente lento para cruzar el viewport; >33: pierde precisión de posicionamiento fino | + +**Resuelto** (ver `design/gdd/datos-de-era-civilizacion.md` Sección Formulas): `world_unit_scale` = 48px/unidad (1 unidad-mundo = 1 tile). A zoom de referencia (1.0), el viewport de 1920px = 40 unidades-mundo de ancho; con `camera_pan_speed` = 30 unidades/seg, un paneo de ancho completo toma ~1.33s (dentro del rango objetivo 1.2–1.5s, y equivalente a 75%/seg, dentro del 60–80%/seg esperado). + +**Nota de aplicación** (ver `design/gdd/control-y-seleccion-de-unidades.md` Fórmula 4): `camera_pan_speed`=30 es el valor a zoom de referencia. La *aplicación* de esta velocidad a otros niveles de zoom la compensa el sistema de Control y Selección de Unidades (dueño de la cámara) mediante `effective_pan_speed_world = camera_pan_speed × zoom`, para mantener la velocidad percibida en pantalla constante. El AC 23 de este documento (medido a zoom 1.0) permanece válido sin cambios. + +### 5. Paso de zoom de cámara +`zoom_step_fraction` = cambio multiplicativo del nivel de zoom por notch de scroll o botón. + +| Variable | Símbolo | Tipo | Default | Rango | Qué se rompe en extremos | +|---|---|---|---|---|---| +| Paso de zoom | `zoom_step_fraction` | float | 0.10 | 0.05–0.15 | <0.05: zoom lento/sluggish; >0.15: saltos "pop" visibles que desorientan | + +**Convención de dirección (fijada en review 2026-08-19 — resuelve la contradicción AC 13↔AC 24):** el escalar de zoom sigue la semántica del GDD dueño de la cámara (`control-y-seleccion-de-unidades.md` Fórmula 2): **`zoom_min` = 0.5 = acercado/táctico** (más px por unidad-mundo), **`zoom_max` = 1.5 = alejado/vista amplia** (encuadre de kaiju a escala completa). Por tanto **zoom-IN reduce el escalar hacia 0.5**, y zoom-OUT lo aumenta hacia 1.5. + +**Mapeo físico:** scroll-up (rueda de mouse arriba) / gatillo-derecho o stick-derecho-arriba (gamepad) = **zoom-IN**. `invert_zoom` (tuning knob) intercambia el mapeo para el jugador que prefiere lo contrario. El escalar de dirección no cambia con `invert_zoom` — solo cambia qué input físico dispara `zoom_in` vs `zoom_out`. + +**Expresiones (dos, una por dirección — no un `±` ambiguo):** + +``` +zoom_in : zoom = clamp(zoom × (1 − zoom_step_fraction), zoom_min, zoom_max) # baja hacia 0.5 +zoom_out: zoom = clamp(zoom × (1 + zoom_step_fraction), zoom_min, zoom_max) # sube hacia 1.5 +``` + +Multiplicativo (no aditivo) para que el cambio percibido escale con la razón, no con el delta absoluto. + +> **Nota de deriva de ida-y-vuelta (no bloqueante, a vigilar):** `(1−f)` y `(1+f)` **no** son inversos exactos: un ciclo in→out multiplica el escalar por `(1−f²)` (a `f`=0.15, −2.25% por ciclo; a `f`=0.05, −0.25%). Un jugador que hace scroll adelante-y-atrás en cantidades iguales verá el zoom derivar levemente hacia `zoom_min` hasta que `clamp()` lo fije. Es benigno (el clamp lo acota) pero si el playtest muestra deriva perceptible, usar el verdadero inverso para zoom-out: `zoom = clamp(zoom / (1 − zoom_step_fraction), …)`. Cubierto por AC 13b. + +**Propiedad del mapeo escalar→`Camera2D.zoom` (contrato, no lo posee Input):** este documento define el **escalar de zoom de diseño**; la traducción a la propiedad `Camera2D.zoom` (Vector2) de Godot 4.6 la posee **Control y Selección de Unidades** (dueño del nodo `Camera2D`, su Regla 5 / Fórmula 2). Ese GDD debe fijar el mapeo de forma que `zoom_min`=0.5 resulte visualmente *acercado* — un signo invertido ahí rompería silenciosamente tanto la resolución clic→unidad (su AC-F4) como la compensación de paneo `effective_pan_speed_world = camera_pan_speed × zoom`. Ver OQ-INPUT-2. + +**Resuelto** (ver `design/gdd/datos-de-era-civilizacion.md` Sección Formulas): `zoom_min` = 0.5 (rango seguro 0.4–0.6, acercamiento táctico), `zoom_max` = 1.5 (rango seguro 1.4–2.0, encuadre de kaiju a escala completa con contexto de batalla). De 0.5 a 1.5 con `zoom_step_fraction` = 0.10 son ~11-12 notches de scroll. + +## Edge Cases + +- **Si un dispositivo de gamepad se desconecta a mitad de partida** (detectado vía `Input.joy_connection_changed`, Regla Núcleo 1): el sistema conserva el último contexto de input activo, emite `input_device_changed(keyboard_mouse)`, solicita pausar el juego y muestra un prompt de reconexión. Esto aplica tanto en consola (estándar de certificación) como en PC — incluso siendo el mouse/teclado el input primario en PC, una desconexión de gamepad a mitad de partida pausa para evitar pérdida accidental de control durante el combate. + - **Excepción crítica — desconexión durante `DEATH_HOLD` (contrato duro con Permadeath):** si la desconexión ocurre mientras un beat `DEATH_HOLD` está activo (`HOLDING`), la solicitud de **pausa real del motor NO se dispara** — `permadeath.md` Regla 4 / AC-P07c prohíben que `SceneTree.paused` se active durante el beat (ningún `RESOLVED` puede ocurrir fuera de la vista del jugador). El sistema **difiere** la solicitud de pausa hasta que Permadeath libere el beat (2–8s), momento en el cual se emite. El prompt de reconexión (que no pausa el motor) sí puede mostrarse inmediatamente, visualmente distinguible del congelamiento del beat para que el jugador no confunda "tu control se desconectó" con "el ritual de muerte está corriendo". Ver AC 17b. +- **Al arranque, antes de cualquier evento de input físico** (`active_input_method` sin valor de input aún): el valor por defecto se determina por **plataforma detectada, no por input** — `KEYBOARD_MOUSE` en PC, `GAMEPAD` en consola. Esto evita que un jugador de PC sin gamepad conectado vea prompts o resaltado de foco estilo-gamepad en el primer frame antes de tocar nada. Es responsabilidad de Input (dueño de la señal). Ver AC 5b. *(Resuelve parcialmente la Open Question de "jugar sin gamepad en PC" en su tramo de primer arranque; el flujo completo de "nunca hay gamepad" sigue en OQ-INPUT-3.)* +- **Si el jugador cambia de método de input (mouse → gamepad) a mitad de acción**: "acción en curso" = **cualquier acción actualmente ligada a un input físico sostenido** (arrastre de box-select, paneo sostenido, zoom sostenido, navegación sostenida en `BLESSING_SELECT`) — no solo box-select. El sistema completa esa acción en curso con el dispositivo original y aplica el nuevo método solo a la siguiente acción; `active_input_method` se actualiza para que la UI refleje el cambio. **Debounce/histéresis (evita flicker):** un cambio de método requiere una **pulsación deliberada** (un `pressed` de acción o una deflexión de stick sostenida ≥ un umbral mínimo de frames), no un roce incidental — apoyar la mano en un gamepad o rozar un stick por debajo de un umbral no debe alternar el método ni la UI de foco dual. El umbral concreto es un contrato que Input posee; los consumidores (p. ej. el cursor virtual de Control) no deben reimplementar su propia histéresis. +- **Si el input llega durante `DEATH_HOLD`**: se **descarta sin encolar** (comportamiento correcto y deliberado — reproducir una orden formada *antes* de la muerte violaría la ficción; **no** se debe bufferear ni reejecutar). Ninguna orden emitida durante el beat sostenido se ejecuta al liberarse. **Pero el descarte no es silencioso a nivel de sistema:** Input expone `input_lockout_active = true` durante todo el `DEATH_HOLD` (ver Visual/Audio Requirements), para que el jugador que instintivamente mashea controles reciba una señal de "input retenido a propósito", no la ausencia de respuesta que se lee como cuelgue. El descarte de la *orden* es total; la *retroalimentación de que fue intencional* es obligatoria — coincide con el congelamiento de UI del art bible (Sección 7.3). +- **Si box-select y un clic simple ocurren en el mismo frame** (arrastre por debajo del umbral): se resuelve como clic simple; el umbral `drag_threshold_px` es el desempate único. +- **Si dos acciones mapeadas al mismo botón físico entran en conflicto** (ejemplo concreto: el clic izquierdo mapea a `select_unit` en el contexto `RTS_COMMAND` y a `ui_accept` en el contexto `MENU_NAVIGATION`): el contexto activo determina cuál acción dispara — nunca ambas simultáneamente, ya que solo un contexto está activo a la vez. Un clic izquierdo en `RTS_COMMAND` emite `select_requested`; el mismo clic en `MENU_NAVIGATION` emite `ui_accept`. +- **Si se abre el menú de pausa durante `BLESSING_SELECT`**: la selección de bendición se cancela (equivalente a `blessing_selection_cancelled`) antes de transicionar a `MENU_NAVIGATION` — no se anidan contextos. +- **Si el jugador mantiene una orden de movimiento presionada mientras se dispara `DEATH_HOLD`**: la orden se descarta; al liberarse el beat y volver a `RTS_COMMAND`, el jugador debe reemitir la orden (no se auto-reanuda). +- **Si un valor de zoom queda fuera de `[zoom_min, zoom_max]` por un cambio de configuración**: se hace clamp al rango válido en el siguiente frame de input, nunca se permite un zoom fuera de límites. +- **Si el jugador hace clic exactamente en el borde entre dos unidades**: el sistema emite la posición de pantalla cruda (`select_requested(screen_pos)`); la resolución de cuál unidad se selecciona pertenece al sistema de Control y Selección de Unidades, no al Input. + +## Dependencies + +**Dependencias hacia arriba (upstream — sistemas que Input necesita):** +- **Ninguna.** Input es un sistema Foundation puro. Solo depende del motor (API de Input de Godot 4.6), no de otros sistemas del juego. + +**Dependientes hacia abajo (downstream — sistemas que dependen de Input):** + +| Sistema | Tipo | Interfaz de datos | +|---|---|---| +| Control y Selección de Unidades | Dura | Consume `select_requested(screen_pos)`, `box_select_requested(rect)`, `move_commanded(screen_pos)`, `pan_requested(delta)`, `zoom_requested(delta)` | +| Reliquias/Bendiciones | Dura | Solicita entrar a `BLESSING_SELECT`; consume `blessing_choice_confirmed(index)`, `blessing_selection_cancelled` | +| Permadeath | Dura | Fuerza contexto `DEATH_HOLD`; Input suprime todo input mientras dure | +| UI/HUD | Dura | Consume acciones `ui_*` en contextos de menú; consume `active_input_method` para feedback de foco dual (4.6) | +| Temporizador de Preparación/Ritual | Suave | No consume input directamente, pero el cambio de fase Preparación→Clímax puede coincidir con cambios de contexto que Input gestiona | + +**Nota bidireccional (actualizada 2026-08-19):** las GDDs de Control y Selección de Unidades, Reliquias/Bendiciones, Permadeath y UI/HUD **ya existen**, y cada una debe registrar "depende de: Input" y respetar los contratos de señal de la Sección C. Estado de registro tras esta revisión: +- Control y Selección de Unidades — ✅ ya lista a Input (contrato confirmado bidireccionalmente en su GDD). +- Permadeath — ✅ lista a Input en su tabla de Dependientes (libera el contexto `DEATH_HOLD`). +- UI/HUD — ✅ fila de Input **añadida** en esta revisión (consume acciones `ui_*`, `active_input_method`, `input_lockout_active`). +- Reliquias/Bendiciones — ✅ fila de Input **añadida** en esta revisión (solicita `BLESSING_SELECT`; consume `blessing_choice_confirmed`/`blessing_selection_cancelled`). +- `systems-index.md` — ✅ Dependency Map actualizado para listar Input como dependencia de UI/HUD y Reliquias/Bendiciones. + +## Tuning Knobs + +| Knob | Default | Rango seguro | Interacción / qué se rompe | +|---|---|---|---| +| `stick_dead_zone` | 0.20 | 0.10–0.30 | Muy alto = pérdida de control fino; muy bajo = deriva. Debe exponerse en opciones de accesibilidad (pads gastados varían) | +| `drag_threshold_px` | 5 | 3–8 | Escala con resolución/DPI. Interactúa con la precisión percibida de box-select | +| `double_click_window_s` | 0.3 | 0.2–0.5 | Debe exponerse en accesibilidad (jugadores con dificultades motoras necesitan ventanas más amplias) | +| `camera_pan_speed` | 30 (world units/sec) | 27–33 | Resuelto vía `world_unit_scale` (48px/unidad, definido en Datos de Era/Civilización). Interactúa con `zoom_step_fraction`: paneo demasiado rápido a zoom lejano marea | +| `zoom_step_fraction` | 0.10 | 0.05–0.15 | Interactúa con `zoom_min`/`zoom_max`: si el paso es grande y el rango angosto, pocos notches cubren todo el rango | +| `zoom_min` | 0.5 | 0.4–0.6 | Límite de acercamiento táctico; interactúa con `camera_pan_speed` (a más cerca, el mismo mundo/seg se siente más rápido) | +| `zoom_max` | 1.5 | 1.4–2.0 | Límite de alejamiento; permite ver el kaiju a escala completa con contexto de batalla (art bible Principio B) | +| `edge_scroll_enabled` | true (PC) / false (gamepad) | bool | El edge-scroll no aplica a gamepad; debe poder desactivarse en PC por preferencia | +| `invert_zoom` | false | bool | Preferencia de accesibilidad/comodidad estándar | + +Varios de estos knobs (`stick_dead_zone`, `double_click_window_s`, `drag_threshold_px`, `invert_zoom`) alimentan directamente el sistema de Accesibilidad (Alpha) — se referencian aquí como fuente de verdad, no se duplican allá. **`drag_threshold_px` se incluye explícitamente** porque su propio modo de falla documentado es el temblor de mano ("<3: temblor de mano causa box-selects accidentales") — omitirlo del conjunto de accesibilidad sería una autocontradicción. La accesibilidad puede requerir un techo de referencia mayor que el rango de tuning del jugador mediano (3–8), igual que `double_click_window_s`; el rango de accesibilidad se coordina con ese sistema, no se limita al rango de feel estándar. + +## Visual/Audio Requirements + +- El Input no renderiza nada por sí mismo, pero expone `active_input_method` (mouse/teclado vs. gamepad) para que otros sistemas ajusten su feedback visual — crítico para el sistema de foco dual de Godot 4.6 (la UI diegética muestra estados de foco distintos según el método activo). +- Feedback de audio: los cambios de contexto de input no producen sonido directamente, pero el sistema debe emitir señales que el sistema de Audio pueda escuchar (ej. entrar a `MENU_NAVIGATION` puede disparar un sonido de UI). El Input provee el disparador, no el sonido. +- Cursor: el sistema debe soportar mostrar/ocultar el cursor de mouse según el contexto (visible en `RTS_COMMAND`/`MENU_NAVIGATION`, **oculto durante `DEATH_HOLD`** — requisito firme, no "potencialmente"). +- **Contrato de feedback de lockout (`DEATH_HOLD`) — obligatorio (Pilar 1).** Durante `DEATH_HOLD`, Input expone `input_lockout_active = true` y **oculta el cursor** (arriba). Input **provee el disparador, no la puesta en escena**: el congelamiento de UI y la desaturación/ducking del art bible **Sección 7.3** (y Héroes A-1 / V-5→V-6) son quienes *muestran* que el momento es un ritual deliberado y no un cuelgue. La razón de que esto sea obligatorio y no cosmético: el descarte total de input en el instante de una muerte de héroe, sin ninguna confirmación de intencionalidad, se lee como "el juego se congeló" justo cuando el Pilar 1 más necesita que la pérdida se sienta legítima. La distinción "estoy bloqueado a propósito" vs "el juego se colgó" debe ser resoluble por el jugador. Ver AC 15b. + +## UI Requirements + +- No tiene pantallas propias, pero es la fuente de las acciones `ui_*` que la UI de menús consume, y de `active_input_method` para el feedback de foco dual. +- Debe existir una pantalla de **remapeo de controles** (opciones) que lea/escriba el InputMap — esta pantalla es responsabilidad de la GDD de UI/HUD, pero depende de que el Input exponga las acciones nombradas de forma legible. + +## Acceptance Criteria + +Formato GIVEN-WHEN-THEN. Clasificación de tipo de story para el gate de evidencia de test indicada por criterio. + +### Regla Núcleo 1: Solo acciones abstractas, sin lectura de dispositivos crudos +1. **GIVEN** el InputMap tiene una acción `move_commanded` ligada a una tecla/botón físico, **WHEN** un sistema consumidor solicita input, **THEN** consulta `Input.is_action_pressed("move_commanded")` o la señal equivalente — nunca `Input.is_key_pressed()`/`Input.is_joy_button_pressed()` directamente. *(Chequeo de code-review/lint — grep de llamadas crudas fuera del directorio del Input System, propiedad de godot-gdscript-specialist, no QA de caja negra.)* +2. **GIVEN** el jugador rebindea una tecla en opciones, **WHEN** usa esa acción en cualquier parte del juego, **THEN** el comportamiento cambia consistentemente sin rutas de dispositivo crudo remanentes. *(Proxy conductual de caja negra para QA.)* + +### Regla Núcleo 2: Contexto único activo, propiedad de la máquina de estados +3. **GIVEN** el sistema está en `RTS_COMMAND`, **WHEN** el sistema de Permadeath emite un evento de muerte, **THEN** `current_context == DEATH_HOLD` y no existe ningún otro flag/estado de contexto en `true` simultáneamente (el contexto es un **único campo enum** `_current_context`, no un conjunto de booleanos — la aserción es sobre ese único campo). *(Unit test del state machine expuesto.)* +4. **GIVEN** el sistema está en `MENU_NAVIGATION`, **WHEN** un sistema de UI intenta setear `current_context` directamente (asignación externa) en vez de llamar `request_context_transition()`, **THEN** el cambio es rechazado **por ausencia de setter** — `current_context` es `get`-only, la asignación externa falla en runtime y `_current_context` permanece en `MENU_NAVIGATION`. *(Unit test: la aserción es que el valor no cambió y que la vía válida es el método de transición.)* + +### Regla Núcleo 3: Rastreo del último método de input +5. **GIVEN** el jugador venía usando mouse/teclado, **WHEN** presiona cualquier botón de gamepad (una **pulsación deliberada**, no un roce por debajo del umbral de debounce), **THEN** el sistema emite `input_method_changed(GAMEPAD)` en el mismo frame en que se registra el input. +5b. **GIVEN** el juego recién arrancó y no se ha registrado ningún evento de input físico, **WHEN** un consumidor consulta `active_input_method`, **THEN** devuelve el default por plataforma detectada — `KEYBOARD_MOUSE` en PC, `GAMEPAD` en consola — nunca un valor sin inicializar ni un default de gamepad en PC. +5c. **GIVEN** el jugador usa el mouse con una mano apoyada en un gamepad, **WHEN** el stick del gamepad se deflecta por **debajo** del umbral de debounce (roce incidental), **THEN** `active_input_method` permanece en `KEYBOARD_MOUSE` y no se emite `input_method_changed` (sin flicker de método). + +### Regla Núcleo 4: Sin lógica de juego, solo señales de intención +6. **GIVEN** el jugador hace clic en la posición de pantalla de una unidad, **WHEN** el clic se registra, **THEN** el Input emite `unit_selected_at(screen_pos)` y no realiza lógica de selección propia (sin lookup de unidad, sin mutación de estado) — la señal carga solo datos de posición/intención cruda. *(Chequeo de code-review.)* + +### Fórmula: stick_dead_zone = 0.20 +7. **GIVEN** contexto `RTS_COMMAND` y un stick de gamepad, **WHEN** el stick se empuja a 0.19 de deflexión máxima, **THEN** no se emite señal de paneo de cámara. +8. **GIVEN** el mismo setup, **WHEN** el stick se empuja a 0.21 de deflexión máxima, **THEN** se emite una señal de paneo de cámara. + +### Fórmula: drag_threshold_px = 5 (a 1080p) +9. **GIVEN** contexto `RTS_COMMAND` a resolución 1080p, **WHEN** el jugador presiona mouse-abajo y suelta dentro de 4px de la posición inicial, **THEN** el sistema emite intención de clic simple, no de box-select. +10. **GIVEN** el mismo setup, **WHEN** el jugador arrastra 6px o más antes de soltar, **THEN** el sistema emite intención de box-select, no de clic simple. + +### Fórmula: double_click_window_s = 0.3 +11. **GIVEN** contexto `RTS_COMMAND`, **WHEN** dos clics sobre la misma unidad ocurren a 0.29s de distancia, **THEN** se emite intención de doble-clic. +12. **GIVEN** el mismo setup, **WHEN** dos clics ocurren a 0.31s de distancia, **THEN** se emiten dos intenciones de clic simple separadas, no un doble-clic. + +### Fórmula: zoom_step_fraction = 0.10 (dirección fijada: zoom-in → hacia zoom_min) +13. **GIVEN** nivel de zoom actual Z dentro de `[zoom_min, zoom_max]`, **WHEN** se registra un input de **zoom-in** (scroll-up con `invert_zoom=false`), **THEN** el nuevo nivel de zoom es `Z × (1 − 0.10)` = `Z × 0.90` (± tolerancia de float), con clamp a `zoom_min` (0.5) si el resultado baja de él — porque zoom-in **acerca** (escalar hacia 0.5), coherente con `control-y-seleccion-de-unidades.md` Fórmula 2. *(Regresión anti-inversión: si un impl hiciera `Z × 1.10` aquí, contradiría AC 24 y la semántica del GDD dueño de la cámara.)* +13a. **GIVEN** nivel de zoom actual Z dentro de `[zoom_min, zoom_max]`, **WHEN** se registra un input de **zoom-out** (scroll-down con `invert_zoom=false`), **THEN** el nuevo nivel es `Z × (1 + 0.10)` = `Z × 1.10`, con clamp a `zoom_max` (1.5) si se excede. +13b. **GIVEN** `invert_zoom=true`, **WHEN** el jugador usa scroll-up, **THEN** se aplica la expresión de **zoom-out** (el escalar de dirección no cambia; solo se intercambia qué input físico dispara cada expresión). + +### Edge Case: supresión de input en DEATH_HOLD (prioridad máxima — dependencia del Pilar 1) +14. **GIVEN** contexto `RTS_COMMAND` con un input de orden-de-movimiento sostenido, **WHEN** el sistema de Permadeath dispara un evento de muerte, **THEN** el contexto transiciona a `DEATH_HOLD` inmediatamente (mismo frame) y la orden sostenida se descarta, no se encola ni ejecuta al soltar. +15. **GIVEN** contexto `DEATH_HOLD` activo, **WHEN** el jugador presiona cualquier acción (movimiento, selección, bendición), **THEN** no se emite ninguna señal de intención, y no se bufferea input para replay al terminar `DEATH_HOLD`. +15b. **GIVEN** contexto `DEATH_HOLD` activo, **WHEN** se inicia el beat, **THEN** `input_lockout_active == true` durante toda su duración y el cursor de mouse está oculto; al liberarse el beat, `input_lockout_active` vuelve a `false` y el cursor reaparece según el contexto de destino. *(Verifica el contrato de feedback de lockout — Pilar 1: el descarte de input debe ser señalizado como intencional, no silencioso.)* +16. **GIVEN** contexto `DEATH_HOLD` activo y un input presionado 100ms antes de la transición y aún sostenido, **WHEN** la transición se completa, **THEN** el input previamente en cola/en vuelo se descarta (verifica "descarta input en cola" de la Sección C). + +### Edge Case: desconexión de gamepad a mitad de partida (consola Y PC) +17. **GIVEN** contexto `RTS_COMMAND` (en consola o PC), **WHEN** el gamepad activo se desconecta (evento `Input.joy_connection_changed(device, false)`), **THEN** el sistema emite una solicitud de pausa y una señal de prompt de reconexión dentro de un frame del evento de desconexión. +17b. **GIVEN** contexto `DEATH_HOLD` activo (beat `HOLDING` sostenido por Permadeath), **WHEN** el gamepad activo se desconecta, **THEN** el sistema emite la señal de prompt de reconexión, pero **NO** dispara/honra una solicitud de pausa real del motor (`SceneTree.paused` permanece `false`) — la solicitud de pausa se **difiere** hasta que Permadeath libere el beat, momento en que se emite. *(Respeta `permadeath.md` Regla 4 / AC-P07c: ningún `RESOLVED` puede ocurrir detrás de una pausa. Un impl literal de AC 17 durante `DEATH_HOLD` violaría el contrato de Permadeath.)* + +### Edge Case: cambio de método de input a mitad de acción +18. **GIVEN** el jugador inicia un arrastre de box-select con mouse, **WHEN** se presiona un input de gamepad antes de soltar el botón del mouse, **THEN** el box-select se completa usando las coordenadas del mouse (dispositivo original) y el input de gamepad no se aplica a la acción en curso. + +### Edge Case: pausa durante BLESSING_SELECT +19. **GIVEN** contexto `BLESSING_SELECT` activo, **WHEN** se registra un input de pausa, **THEN** la selección de bendición se cancela (se emite intención de cancelar) antes/al transicionar de contexto. + +### Edge Case: conflicto de botón resuelto por contexto +20. **GIVEN** el clic izquierdo mapea a `select_unit` en `RTS_COMMAND` y a `ui_accept` en `MENU_NAVIGATION`, **WHEN** el jugador hace clic izquierdo en `RTS_COMMAND`, **THEN** se emite `select_requested` y no `ui_accept`; **WHEN** hace el mismo clic en `MENU_NAVIGATION`, **THEN** se emite `ui_accept` y no `select_requested`. + +### Edge Case: clic en borde entre unidades +21. **GIVEN** contexto `RTS_COMMAND`, **WHEN** el jugador hace clic en una posición exactamente en el límite visual entre dos unidades, **THEN** el sistema emite `unit_selected_at(screen_pos)` solo con las coordenadas crudas — no resuelve ni emite cuál unidad fue seleccionada. + +### Performance +22. **GIVEN** el juego corriendo a 60 FPS (presupuesto de frame 16.6ms) **en hardware objetivo**, **WHEN** se registra cualquier evento de input físico por el OS, **THEN** la señal de intención correspondiente se emite dentro del mismo frame (≤1 frame / ≤16.6ms de latencia), medido en una muestra de 60 segundos con 0 eventos caídos o retrasados por 2+ frames. + - **Tipo: Integration/Performance (on-device), NO unit test headless.** Esta AC mide latencia real de OS/driver, que un runner headless no puede reproducir (inyectar `InputEvent` sintéticos vía `Input.parse_input_event()` mide solo el pipeline interno, no la latencia física que es el punto). Evidencia: medición instrumentada en hardware objetivo en `tests/performance/input/`, no `tests/unit/input/`. Sigue siendo **BLOQUEANTE** para sign-off de capa Foundation (dependencia del Pilar 1), pero con ruta de evidencia on-device. + - **Mecanismo de medición (requerido, no asumido):** un harness que registre el timestamp del evento en el callback `_input`/`_unhandled_input` y el timestamp de la emisión de la señal de intención, sobre la misma muestra de 60s, contando (a) eventos con `Δframe > 1` y (b) eventos coalescidos/descartados. La captura debe considerar que múltiples eventos de OS pueden llegar entre frames (varios `mouse_motion`/poll de gamepad) — el criterio "0 caídos" exige definir qué coalescencia es aceptable (p. ej. coalescer motion es OK; descartar un `pressed` no lo es). + - **Presupuesto de frame de Input (gap flageado):** ningún renglón de "Input" existe hoy en la tabla de presupuesto de frame-time del estudio. Requiere decisión de `technical-director` sobre cuántos de los 16.6ms puede consumir Input y dónde se rastrea. Ver OQ-INPUT-4. + +### Fórmula: camera_pan_speed = 30 world units/sec (resuelto vía world_unit_scale) +23. **GIVEN** contexto `RTS_COMMAND` a zoom de referencia (1.0) y resolución 1080p (viewport = 40 unidades-mundo de ancho), **WHEN** el jugador mantiene presionado un input de paneo durante exactamente 1.33 segundos, **THEN** la cámara se ha desplazado 40 unidades-mundo (± tolerancia de float), cruzando el ancho completo del viewport. + +### Fórmula: zoom_min/zoom_max = 0.5/1.5 +24. **GIVEN** el nivel de zoom está en `zoom_min` (0.5), **WHEN** el jugador registra un input de zoom-in adicional, **THEN** el nivel de zoom permanece en 0.5 (clamp, no se acerca más — coherente con la convención zoom-in→hacia 0.5). +25. **GIVEN** el nivel de zoom está en `zoom_max` (1.5), **WHEN** el jugador registra un input de zoom-out adicional, **THEN** el nivel de zoom permanece en 1.5 (clamp, no se aleja más). +24b. **GIVEN** un cambio de configuración deja el valor de zoom fuera de `[zoom_min, zoom_max]` (p. ej. una config cargada con zoom = 2.0), **WHEN** se procesa el siguiente frame de input, **THEN** el valor se clampa al rango válido (1.5) en ese frame — nunca se permite un zoom fuera de límites (cubre el edge case de "valor de zoom fuera de rango por cambio de configuración" de la Sección D, distinto del clamp por input del jugador de AC 24/25). + +### Clasificación de tipo de story (para el gate de evidencia) +- Reglas núcleo (AC 1-6), fórmulas y edge cases de DEATH_HOLD (AC 14-16, 15b) → tipo **Logic** → BLOQUEANTE, requieren unit tests automatizados en `tests/unit/input/`. +- **AC 5b (default de arranque por plataforma), 5c (debounce), 19 (pausa en BLESSING_SELECT), 20 (conflicto de botón por contexto), 21 (clic en borde)** → tipo **Logic** → requieren unit tests en `tests/unit/input/`. *(Clasificación añadida en review 2026-08-19 — antes sin tipo asignado, por tanto sin evidencia requerida.)* +- Desconexión de gamepad (AC 17), **AC 17b (desconexión durante DEATH_HOLD)**, cambio de método de input a mitad de acción (AC 18) → tipo **Integration** → BLOQUEANTE, requieren test de integración o playtest documentado. +- **AC 15b (feedback de lockout)** → tipo **Visual/Feel** → ADVISORY (screenshot + sign-off del lead): el *estado* `input_lockout_active`/cursor es Logic-testeable, pero que "se lea como intencional, no como cuelgue" es de feel, a validar en playtest. +- **Performance (AC 22)** → tipo **Integration/Performance (on-device)** → BLOQUEANTE para sign-off de capa Foundation → evidencia en `tests/performance/input/`, **no** unit test headless (ver la nota de AC 22). +- AC 23 (camera_pan_speed), 13/13a/13b (dirección de zoom), 24/24b/25 (zoom clamp) → tipo **Logic** → BLOQUEANTE, requieren unit tests en `tests/unit/input/` (dependen de `world_unit_scale` de Datos de Era/Civilización). + +## Open Questions + +| Pregunta | Owner | Resolución objetivo | +|---|---|---| +| ~~`world_unit_scale` no está definido~~ | ✅ Resuelto en `design/gdd/datos-de-era-civilizacion.md` (48px/unidad) | 2026-07-23 | +| ~~`zoom_min`/`zoom_max` sin definir~~ | ✅ Resuelto en `design/gdd/datos-de-era-civilizacion.md` (0.5 / 1.5) | 2026-07-23 | +| **OQ-INPUT-1 — Invariante Pilar 1 distribuida.** Contratos requeridos: (a) la resolución clic→unidad de Control no debe convertir un clic ambiguo en orden letal; (b) el orden de frame Input→Control→Permadeath para una orden de rescate de último segundo vs. la evaluación de muerte debe especificarse (un accidente de orden-de-update no puede ser indistinguible de una decisión legítima). | game-designer + technical-director (cross-GDD: Input / Control / Permadeath) | Antes de `/create-architecture` — candidato a ADR | +| **OQ-INPUT-2 — Mapeo escalar→`Camera2D.zoom`.** El GDD dueño de la cámara (Control y Selección de Unidades) debe fijar el mapeo de forma que `zoom_min`=0.5 sea visualmente *acercado*, sin invertir el signo (rompería su AC-F4 y la compensación de paneo). Verificar la convención de `Camera2D.zoom` contra docs de 4.6. | godot-specialist + Control y Selección de Unidades | Al fijar el contrato de cámara, antes de implementar | +| **OQ-INPUT-3 — Flujo "jugar sin gamepad conectado" en PC.** El default de primer arranque ya está resuelto (AC 5b: `KEYBOARD_MOUSE` en PC). Falta el flujo completo cuando *nunca* hay gamepad (el cursor virtual nunca aparece — confirmar que basta). | GDD de UI/HUD + este sistema | Antes de implementar el input de consola | +| **OQ-INPUT-4 — Presupuesto de frame-time de Input.** No hay renglón "Input" en la tabla de presupuesto del estudio; decidir cuántos de los 16.6ms puede consumir y dónde se rastrea la latencia (gate AC 22). | technical-director | Antes de sign-off de capa Foundation | +| **OQ-INPUT-5 — ADR de enforcement de `current_context`.** El patrón `get`-only + `request_context_transition()` es un contrato cross-cutting (UI/HUD, Reliquias, Permadeath); documentarlo en un ADR. | godot-gdscript-specialist | Antes de `/dev-story` del sistema de Input | diff --git a/design/gdd/permadeath.md b/design/gdd/permadeath.md new file mode 100644 index 0000000000..b9b492526b --- /dev/null +++ b/design/gdd/permadeath.md @@ -0,0 +1,390 @@ +# Permadeath + +> **Status**: In Design — 2026-08-16: pasada de reconciliación cross-GDD (OQ-P6) completa — los 6 ítems se aplicaron a Sistema de Héroes, Guardado/Persistencia, Encuentro con Kaiju y Datos de Era/Civilización (ver OQ-P6, OQ-P1 ✅ resueltos; ver también el parche cross-GDD 2026-08-16 en cada uno de esos GDDs). Previo: re-review 2026-08-15 (NEEDS REVISION): 12 bloqueantes previos confirmados resueltos; 4 bloqueantes nuevos aplicados en esa pasada (alcance del cap de Regla 8 bajo el path de timeout, reclasificación AC-P01/P02/P06b, gap de `reset_queue()` en reload a mitad de era, spike de motor elevado a gate de Approved). **Pendiente para Approved**: spike de motor 4.6 (OQ-P5, ya gate explícito), OQ-P2 (señal `animation_done`/hook de revelación forzada), OQ-P3/AC-P16 (Forja de Legado sin GDD), y una re-review formal en sesión limpia para confirmar que la reconciliación no introdujo problemas nuevos. +> **Author**: usuario + agentes +> **Last Updated**: 2026-08-15 +> **Implements Pillar**: Sacrificio con Peso (Pilar 1); El Pasado es Poder (Pilar 2) + +## Overview + +Permadeath es el sistema que **sella** la muerte de un héroe y actúa como el punto de entrega entre dos sistemas ya diseñados: recibe el payload de circunstancia (`hero_id`, `hero_name`, `relic_category`, `relic_quality` ya computada por Héroes, y D/T/P snapshoteados que la acompañan como evidencia) que Sistema de Héroes expone al cruzar `DYING`, controla el *timing* del beat sostenido `DEATH_HOLD` que el jugador vive en carne propia — la pantalla se detiene, el mundo se aparta, no hay vuelta atrás — y, al liberar ese beat, confirma `DEAD` de forma irreversible y entrega `relic_category` + `relic_quality` a Forja de Legado. A nivel de datos es una capa delgada de orquestación: no recalcula la calidad de la reliquia (eso ya lo resolvió Héroes vía Fórmula H4), no decide *cuándo* alguien muere (eso lo decide Combate/Daño), y no diseña el arte del beat (eso es de Héroes V-5/V-6) — su responsabilidad es el *cuándo del beat* y *qué pasa con el resultado*, no el *cómo se ve* ni el *cuánto vale*. A nivel de jugador, es el sistema que hace literal la palabra "permadeath": mientras el beat corre, todo input queda suprimido (`DEATH_HOLD`, ya fijado por Input) y no existe ningún camino — ni siquiera un crash del juego — que regrese al héroe a la vida. Sin este sistema, Sistema de Héroes tendría una fórmula de calidad de reliquia sin dueño que la vuelva irreversible, y Forja de Legado no tendría de dónde recibir su input: es el gozne entre el **Pilar 1** (la muerte pesa) y el **Pilar 2** (la muerte se vuelve poder). + +## Player Fantasy + +**El jugador es quien presencia el sellado irrevocable de una decisión que ya tomó.** (Precisión: la decisión se cerró silenciosamente un instante antes, en el snapshot de D/T/P al cruzar `health≤0` en medio del ruido del combate — Héroes H5/AC-H15; el beat `DEATH_HOLD` no es el instante *de decidir* sino el ritual de 2-8s en que ese "para qué" se vuelve visible e irreversible ante los ojos del jugador. La agencia ya pasó; el beat la sella y la muestra.) Si Sistema de Héroes es la fantasía de *decidir* para qué muere un héroe, Permadeath es la fantasía —más precisa, más incómoda— de *mirar ese "para qué" sellarse* sin posibilidad de arrepentimiento: no hay pausa-para-pensarlo-mejor, no hay ventana de cancelar, no hay "deshacer". Durante el beat `DEATH_HOLD` el jugador pierde literalmente el control — el Input queda suprimido — y esa impotencia deliberada es el punto: la decisión ya se tomó (en Héroes, en los ~30s previos), y ahora solo queda ver cómo se resuelve. Es el peso del **Pilar 1** hecho mecánica de input, no solo narrativa: *"si dudamos entre dar un reintento gratuito o dejar que la caída del héroe sea definitiva y genere legado, elegimos que sea definitiva"* — Permadeath es, literalmente, el sistema que hace cumplir esa frase a nivel de código: no expone ningún método que revierta `DEAD`, ni siquiera ante un crash (contrato ya fijado por Héroes AC-H27). + +Al mismo tiempo, hay una capa que el jugador nunca ve directamente: la orquestación de *cuándo* se libera el beat, cómo se resuelve una muerte doble en el mismo frame, y cómo el resultado (`relic_category` + `relic_quality`) viaja hacia Forja de Legado. Esa capa no tiene fantasía propia — no es donde vive la emoción — pero es la que garantiza que la fantasía de arriba sea *honesta*: si el timing fuera arbitrario o el hand-off perdiera datos en el camino, la "irreversibilidad" sería una promesa vacía en vez de una regla que el jugador puede confiar. + +**Test de diseño (este sistema debe hacerlo cumplir):** si algún input del jugador lograra alterar D/T/P o `relic_quality` *después* de que `DEATH_HOLD` comenzó, el diseño está roto — el momento de agencia ya pasó (y debe seguir habiendo pasado; ver Héroes AC-H15/U-4, el freeze del snapshot). Permadeath no reabre esa ventana, solo administra lo que pasa una vez cerrada. + +## Detailed Design + +### Core Rules + +1. **Único punto de confirmación de `DEAD`**: Permadeath es el único sistema que confirma la transición `DYING → DEAD` y libera el beat `DEATH_HOLD`. Cuando Sistema de Héroes transiciona un héroe a `DYING`, **Héroes evalúa la Fórmula H4 en el frame del snapshot** (donde D/T/P ya están congelados, Héroes H5/AC-H15) y expone a Permadeath un payload de circunstancia que incluye la `relic_quality` **ya computada**: `hero_id`, `hero_name`, `relic_category`, `relic_quality`, y los D/T/P snapshoteados que la acompañan como evidencia. Permadeath **nunca computa, recalcula ni muta** `relic_quality` ni D/T/P — solo los transporta. *(Contrato cross-GDD: esto requiere que Héroes AC-H12 exponga `relic_quality` en el payload, no solo D/T/P — ver Open Questions OQ-P6 y la nota de acción cross-GDD al final de Dependencies.)* + +2. **Timing híbrido del beat (piso + señal + timeout)**: al recibir un payload, Permadeath inicia `DEATH_HOLD` para ese héroe y lo libera cuando se cumplen **ambas** condiciones: (a) transcurrió un piso mínimo fijo `death_hold_min_duration_s` (ritmo consistente, evita un beat que se sienta cortado); (b) Sistema de Héroes notificó el fin de su animación de resolución (V-5→V-6: silueta agrietándose → ícono de reliquia). Si la señal de fin de animación no llega dentro de `death_hold_timeout_s` (techo de seguridad, mayor que el piso), el beat se libera igual — con un log de advertencia en build de desarrollo — para que un bug de animación nunca cuelgue la partida indefinidamente. **Incluso en el path de timeout, la revelación del ícono de reliquia (V-6) debe forzarse a su estado final visible** — Permadeath entrega a Héroes/Forja la señal de que el resultado debe mostrarse en su end-state aunque la animación intermedia se haya perdido; un `RESOLVED` nunca puede ocurrir sin que el jugador vea que la reliquia se forjó (protege el payoff del Pilar 2; ver PV-5 y AC-P06b). + +3. **Cola FIFO de muertes pendientes**: los payloads de muerte que llegan se encolan en orden de llegada (tiebreak determinístico por `hero_id` si dos notificaciones llegan en el mismo frame — mismo caso que Héroes AC-H24, snapshots independientes). Permadeath solo mantiene **un** beat `DEATH_HOLD` activo a la vez (satisface Héroes AC-H25: nunca dos beats sostenidos simultáneos); las muertes adicionales esperan su turno en la cola sin alterar su propio payload ya congelado. + + **Presentación del héroe en `QUEUED` (el jugador presencia cada muerte, no la triage):** un héroe encolado NO reproduce todavía su animación de colapso (V-5) mientras espera — **se sostiene en el umbral de colapso** (última pose viva/congelado en el frame de cruce, dentro de la pausa global que ya detuvo el mundo), y su beat completo y presenciado (V-5→V-6) se reproduce **solo cuando entra a `HOLDING`**. Esto protege la Player Fantasy central ("el jugador es el testigo de cada muerte"): ninguna muerte se colapsa fuera de campo mientras el jugador mira otra, ni queda un cadáver congelado a mitad de colapso. El costo es que, con 2-3 muertes apiladas, el jugador presencia una secuencia de beats en vez de un evento simultáneo — mitigado por la Regla 8 (cap de bloqueo total). + + **Decisión de diseño (2026-08-14): sin "tell" diegético sobre el héroe en `QUEUED`.** Se consideró añadir un temblor/brillo sostenido para que el jugador perciba que hay una muerte pendiente (y no lea la pose congelada como un hitch gráfico), pero se rechazó: reintroduciría un elemento de puesta en escena que Permadeath no posee y rozaría el understatement anti-melodrama de PU-3 (no se editorializa que una muerte "está por venir"). La pose se sostiene limpia; se re-evalúa en playtest solo si los datos muestran que la secuencia se lee como fallo técnico en vez de reverencia. + +4. **Pausa global de tiempo de juego durante `DEATH_HOLD` (cooperativa, NO `SceneTree.paused`)**: al iniciar un beat, Permadeath expone `game_time_paused = true` — Combate/Daño (AC-C52) y Encuentro con Kaiju deben leer este flag y pausar cooldowns, telegrafíos y ataques mientras dure. Justificación: el Input ya está suprimido durante `DEATH_HOLD` (contrato de Héroes), así que dejar correr el resto del combate produciría daño inevitable que el jugador no puede prevenir ni reaccionar — rompería la legibilidad que Combate/Daño exige como condición de su propia Player Fantasy. Al liberar el beat (Regla 2), `game_time_paused` vuelve a `false`. + + **Mecanismo de pausa (fija el contrato de implementación, no lo deja a interpretación):** la "pausa global" es **cooperativa por polling del flag** — Permadeath **nunca** invoca la pausa real del motor (`SceneTree.paused = true` en Godot 4.6). Cada consumidor (Combate, Kaiju, y ver OQ-P4) decide por sí mismo no avanzar su lógica mientras lee `game_time_paused == true`. Motivo: si Permadeath usara la pausa real del motor, (a) la animación de resolución del héroe (V-5→V-6 de Héroes, que alimenta `animation_done` de la Fórmula P1) se congelaría junto con todo lo demás salvo que su nodo esté en `PROCESS_MODE_ALWAYS` — y entonces `animation_done` nunca llegaría, haciendo que **toda** muerte caiga al path de timeout de 8s (enmascarando el caso normal de AC-P04); y (b) un consumidor en `PROCESS_MODE_PAUSABLE` dejaría de ejecutar su `_process` y no podría **leer** el flag que se le pide leer. El único nodo que corre exento de la pausa es el que administra el beat (`PROCESS_MODE_ALWAYS`, ver Edge Cases); todo lo demás pausa cooperativamente. *(El patrón concreto — quién posee el flag, cómo lo publica — es parte del ADR de OQ-P5.)* + + **Interacción con la pausa REAL del motor (menú de pausa global) durante un beat:** el menú de pausa global de Guardado/Temporizador (AC-T22) sí usa la pausa real del motor (`SceneTree.paused = true`). Si esa pausa se activara a mitad de un `HOLDING`, un beat que corre en `PROCESS_MODE_ALWAYS` seguiría avanzando `elapsed_s` y podría **resolverse (`DEAD` + hand-off a Forja) detrás del menú, fuera de la vista del jugador** — anulando la Player Fantasy ("presencia cada muerte") y el Pilar 1. **Decisión de diseño (2026-08-14): la acción de abrir el menú de pausa global queda suprimida durante `HOLDING`**, junto con el resto del Input ya suprimido (`DEATH_HOLD`). El beat es inevitable e ininterrumpible por diseño (Pilar 1, PU-2) y dura solo 2-8s; por tanto, una pausa real del motor **nunca puede iniciarse** mientras hay un beat activo, y ningún `RESOLVED` puede ocurrir con `SceneTree.paused == true`. (Ver AC-P07c e Input en Interactions.) *(Corolario que este GDD exige a Héroes: el nodo de animación de resolución V-5→V-6 debe correr en `PROCESS_MODE_ALWAYS` — requisito hoisteado a la lista de acciones cross-GDD, OQ-P6.)* + +5. **Confirmación de `DEAD` y hand-off del veredicto**: al liberarse el beat de un héroe, Permadeath invoca la confirmación de `DEAD` (la escritura del campo de estado la posee Héroes — Permadeath solo llama al método, mismo patrón que Combate no-posee-estado) y entrega a Forja de Legado el veredicto final: `hero_id`, `hero_name`, `relic_category`, `relic_quality`. Esta entrega ocurre una única vez por héroe, de forma síncrona con la liberación del beat. + +6. **No posee autosave, pero el autosave debe persistir el payload completo (para la recuperación por crash)**: el autosave obligatorio se dispara en la transición a `DYING`, propiedad de Sistema de Héroes (AC-H26) — ocurre *antes* de que Permadeath reciba el payload. Permadeath no duplica ese disparo. **Requisito nuevo sobre ese autosave (para cerrar la ventana de crash de la Regla 8b / AC-P18):** el snapshot persistido en `DYING` debe incluir el **payload de muerte completo** (`hero_id`, `hero_name`, `relic_category`, `relic_quality` ya computada, D/T/P) marcado como *muerte pendiente de sellar*, no solo `state = DYING`. Así, un crash a mitad del beat no pierde la reliquia: al recargar, el sistema coacciona `DYING → DEAD` y forja la reliquia desde el payload persistido (ver Regla 8b y AC-P18). *(Contrato cross-GDD: Guardado/Persistencia debe persistir este payload; su tabla de Interactions atribuye hoy el disparo del autosave a Permadeath — es una imprecisión a corregir, el dueño del disparo es Héroes en `DYING`. Ver nota de acción cross-GDD al final de Dependencies.)* + +7. **No evalúa victoria/derrota de era** (mismo hueco que Héroes/Combate, ya resuelto en general): Encuentro con Kaiju posee esa evaluación (Regla 9). Permadeath solo notifica cada `DEAD` confirmado a sus consumidores declarados (Forja de Legado); una racha de muertes que vacíe el roster de héroes vivos es observada por Encuentro con Kaiju, no decidida aquí. + +8. **Cap de bloqueo total por encuentro (protege el ritmo del Pilar 3) — compresión UNIFORME y determinística**: con 2-3 héroes/era, varias muertes apiladas producirían una secuencia de beats totalmente pausados e input-suprimidos justo dentro del clímax explosivo (Pilar 3). Para acotar el peor caso, el tiempo **total** de los pisos de una cadena de muertes se topa con `death_hold_total_lockout_cap_s` (tuning knob). + + **`N` es fijo al formarse la cadena — una cadena NO puede crecer a mitad de vuelo (esto es lo que hace el cap matemáticamente honesto):** una "cadena" es el conjunto de muertes que se encolan en el mismo instante de formación (típicamente, el frame de un multi-kill: un golpe de área del kaiju que cruza `health≤0` de 2-3 héroes a la vez). **Ninguna muerte puede unirse a una cadena una vez que el primer beat entró a `HOLDING`**, porque la Regla 4 (`game_time_paused`) congela **todas** las fuentes de daño de tiempo de juego mientras cualquier beat está activo — ningún héroe puede cruzar `health≤0` durante un beat, así que no existe el evento de daño que produciría una "muerte tardía". Por tanto `N` = número de muertes de la cadena queda **congelado en el frame de formación** y nunca se recomputa. *(Corolario de dependencia: esta garantía descansa sobre que **todas** las fuentes de daño respeten `game_time_paused` — Combate ya lo hace, AC-C52; Kaiju es el contrato residual pendiente, OQ-P1/AC-P14. Si Kaiju **no** congelara su ciclo durante el beat, podría landear un golpe letal a mitad de `HOLDING` y romper este invariante; por eso OQ-P1 no es solo cosmético, es lo que sostiene el cap.)* + + **Algoritmo (determinístico, sin favoritismo de orden), sobre `N` fijo:** si `N × death_hold_min_duration_s ≤ cap`, cada beat usa el piso normal `death_hold_min_duration_s` (sin compresión). Si excedería el cap, **todos** los beats de la cadena usan el **mismo** piso comprimido: + + `piso_efectivo = clamp(death_hold_total_lockout_cap_s / N, death_hold_min_compressed_s, death_hold_min_duration_s)` + + Como `N` está congelado desde antes de que arranque el primer beat, todos los beats se computan con el mismo `N` y reciben el mismo piso; la suma es exactamente el cap cuando `cap/N` cae dentro del rango del clamp, y **nunca lo excede** (la cota inferior del clamp nunca se alcanza si la cross-validación al cargar se cumple — ver abajo). **Ningún beat se singulariza** — ni el primero ni el último ni por `hero_id` — respetando el anti-editorializado de PU-3 y la decisión de "no hay knob de orden". Cada beat sigue respetando `animation_done`/timeout **por encima** de su piso. *(`N` está garantizado `≥ 1` por el contexto de invocación — un beat solo se computa cuando existe al menos una entrada en la cadena; aun así, la implementación debe asertar `N ≥ 1` antes de dividir, para fallar ruidosamente ante un bug de invocación en vez de dividir por cero.)* + + **Cross-validación al cargar / al activar cada era (loud-fail, coherente con la política del proyecto):** el cap debe ser satisfacible para el peor caso de roster **de cada era**. Se valida `death_hold_total_lockout_cap_s ≥ N_max × death_hold_min_compressed_s` **y** `death_hold_total_lockout_cap_s ≥ death_hold_min_duration_s`, donde `N_max` = tamaño máximo de roster de héroes **de la era que se está activando**, leído desde Datos de Era/Civilización. Como el roster **varía por civilización/era**, esta validación **se re-ejecuta cada vez que una era alcanza `LOADED`** (no una sola vez al arranque global): la lectura de `N_max` se dispara sobre la señal de era-`LOADED` de Datos de Era, nunca en el `_ready()` de Permadeath (evita una carrera contra la máquina de estados `LOADING→LOADED` de esa era — ver OQ-P5 y OQ-P6). Si cualquiera de las dos desigualdades falla → falla ruidosamente en ese punto, nunca se clampa el cap en silencio (un cap infeasible dejaría la promesa de AC-P21 matemáticamente rota). Con `N` fijo y `N ≤ N_max`, `cap/N ≥ cap/N_max ≥ death_hold_min_compressed_s`, de modo que la cota inferior del clamp nunca se activa y la suma de pisos **nunca** supera el cap — no existe el caso de arribo escalonado que antes lo rompía. El valor exacto del cap se afina en playtest. + + **Alcance de la garantía del cap: camino diseñado, no camino con bug de animación (decisión 2026-08-15).** La suma de pisos `Σ piso_efectivo` nunca excede el cap (demostrado arriba) — pero un beat individual sigue respetando `animation_done`/timeout **por encima** de su piso (Regla 2), y el cap no acota ese timeout. Si uno o más beats de una cadena caen al path de timeout de `death_hold_timeout_s` (bug de animación de Héroes, ya logueado como advertencia en build de desarrollo por AC-P06), el tiempo total de la cadena **puede exceder** `death_hold_total_lockout_cap_s`. Esto es una decisión consciente, no un descuido: acortar el timeout de los beats comprimidos para que el cap se sostenga incluso en el camino con bug reintroduciría el riesgo de ventana sub-frame que el margen mínimo de animación (`death_hold_min_animation_window_s`, AC-P08) existe específicamente para prevenir — comprimir el timeout junto con el piso apretaría esa ventana justo en el escenario (cadenas de 2-3 muertes) donde AC-P08 más importa. **La garantía del cap cubre el camino diseñado** (todos los beats de la cadena resuelven vía `animation_done`, sin caer a timeout) — la misma tolerancia a bugs de animación ya aceptada para un solo beat (AC-P06 acepta hasta 8.0s de espera silenciosa) se hereda sin cambios a una cadena: si el bug ocurre repetidamente dentro de una cadena, el peor caso de bloqueo total crece más allá del cap, igual que ya podía crecer más allá del piso normal para un solo beat antes de esta regla existir. No hay knob nuevo ni cambio a P1/AC-P08 — ver AC-P21 para la cláusula de alcance correspondiente. + + **Costo de feel a vigilar en playtest (Pilar 1 vs Pilar 3, decisión 2026-08-14: se mantiene el cap):** con la compresión al piso (`death_hold_min_compressed_s=1.2`), una cadena de 3 muertes produce tres beats casi idénticos y muy cortos que pueden **leerse como una cola de UI drenándose en vez de tres muertes presenciadas** (no hay diferenciación presentacional beat-a-beat — misma duración, misma pose, sin sting por héroe per PU-3). Se eligió conscientemente el cap sobre un "piso de legibilidad que deje exceder el cap" (Pilar 3 gana el peor caso raro). **Métrica de playtest**: si los datos de sesiones con multi-kills muestran que el jugador deja de registrar cada muerte como evento individual (p. ej. no puede nombrar a los 3 héroes caídos tras la cadena), re-evaluar subir `death_hold_min_compressed_s` o el cap antes de considerar un piso de legibilidad. Ver PV-2. + +8b. **Recuperación por crash: coacción `DYING → DEAD` al recargar (cierra la ventana de la Regla 6)**: Permadeath no persiste estado de cola propio en runtime, pero **sí depende** de que el payload de muerte se haya persistido en el autosave de `DYING` (Regla 6). Al recargar tras un crash ocurrido en cualquier punto del beat (`HOLDING`) o antes de que Forja completara, el flujo de carga detecta cualquier héroe persistido como *muerte pendiente de sellar* (`DYING` con payload) y lo **coacciona a `DEAD`**, forjando su reliquia desde la `relic_quality` persistida — sin reanudar ni re-reproducir el beat visual (el beat es una experiencia de sesión, no un estado a restaurar). Esto hace verdadera la promesa de AC-P18/Héroes AC-H27 (irreversible, siempre produce reliquia) que antes quedaba sin dueño. *(Contrato cross-GDD: la coacción al cargar la posee el flujo de carga de estado del héroe — Héroes + Guardado; Permadeath define el requisito, no la escritura. Ver nota de acción cross-GDD al final de Dependencies.)* + +### States and Transitions + +*(Estado por entrada en la cola de muertes pendientes — no es el estado del héroe en sí, que sigue siendo `DYING`/`DEAD` propiedad de Héroes.)* + +| Estado | Descripción | Transición desde | Transición hacia | +|---|---|---|---| +| `QUEUED` | Payload recibido de Héroes, esperando turno en la cola FIFO; el héroe se sostiene en el umbral de colapso (aún NO reproduce V-5) dentro de la pausa global (Regla 3) | Notificación `DYING` de Héroes | `HOLDING` (cuando le toca turno y no hay otro `HOLDING` activo) | +| `HOLDING` | Beat `DEATH_HOLD` activo: pausa global de tiempo de juego, input suprimido, esperando piso mínimo + señal de animación (o timeout) | `QUEUED` (le llega el turno) | `RESOLVED` | +| `RESOLVED` | `DEAD` confirmado, veredicto entregado a Forja de Legado; entrada removida de la cola | `HOLDING` | — (terminal, se remueve de la cola) | + +### Interactions with Other Systems + +| Sistema | Dirección | Interfaz | +|---|---|---| +| Sistema de Héroes | bidireccional | Héroes expone el payload de muerte al entrar a `DYING`; Permadeath invoca la confirmación de `DEAD` al liberar el beat. Permadeath espera la señal de fin de animación (V-5/V-6) de Héroes para su Regla 2 | +| TimeControl (servicio Core) | Permadeath → TimeControl | **Permadeath solicita el régimen `PAUSED`** (`request_regime(source_id, PAUSED)`) al entrar un beat a `HOLDING`, y lo **libera** (`release_regime`) al `RESOLVED` (ADR-0002, Accepted). Es la única fuente de pausa del MVP. **Permadeath ya NO publica `game_time_paused` por sí mismo** — TimeControl es el dueño del flag y del régimen | +| Combate/Daño | vía TimeControl | Combate lee `game_time_paused` de **TimeControl** (AC-C52) para pausar cooldowns durante `HOLDING`; Permadeath solo solicita el régimen, no expone el flag directamente | +| Encuentro con Kaiju | vía TimeControl | Kaiju lee `game_time_paused` de **TimeControl** (Regla 11/AC-K89); no telegrafía/ataca mientras el régimen efectivo sea `PAUSED` | +| Reliquias/Bendiciones | vía TimeControl (precedencia) | Si un héroe muere durante un `OFFERING`, la solicitud `PAUSED` de Permadeath **supera** a la `SLOWED` de Reliquias (precedencia `PAUSED>SLOWED`, ADR-0002); la oferta se preserva y reanuda su cámara lenta al liberar la pausa | +| Input | Permadeath → Input | Permadeath controla cuándo se libera el contexto `DEATH_HOLD` que Héroes ya disparó al entrar a `DYING`. La supresión de `DEATH_HOLD` **incluye la acción de abrir el menú de pausa global** — ninguna pausa real del motor puede iniciarse durante el beat (Regla 4, AC-P07c) | +| Guardado/Persistencia | ninguna directa | El autosave ya lo disparó Héroes en `DYING` (AC-H26), antes de que Permadeath actúe — sin interfaz nueva | +| Forja de Legado | Permadeath → Forja | Al `RESOLVED`, entrega `hero_id`, `hero_name`, `relic_category`, `relic_quality` — input de creación de reliquia (Forja, sin GDD aún) | + +> **Nota de contrato (actualizada 2026-08-21, ADR-0002 Accepted)**: `game_time_paused` es ahora propiedad del servicio Core **TimeControl**, no de Permadeath. Permadeath *solicita* el régimen `PAUSED`; Combate (AC-C52), Kaiju (Regla 11/AC-K89) e Input leen el flag desde TimeControl. La arista Permadeath↔Kaiju que antes era "contrato provisional" queda cableada vía el hub TimeControl. + +## Formulas + +Permadeath es un sistema de orquestación de estado; su única lógica numérica es la **condición de liberación del beat** y sus constantes de timing. No hay curvas de balance (la calidad de la reliquia ya la resolvió Héroes vía H4 — Permadeath no la recalcula). + +### Fórmula P1 — Condición de liberación de `DEATH_HOLD` + +El beat se libera cuando ha transcurrido el piso mínimo **Y** (la animación de resolución terminó **O** se alcanzó el timeout de seguridad): + +`release = (elapsed_s ≥ death_hold_min_duration_s) AND (animation_done OR elapsed_s ≥ death_hold_timeout_s)` + +| Variable | Símbolo | Tipo | Rango | Descripción | +|----------|---------|------|-------|-------------| +| Tiempo en `HOLDING` | `elapsed_s` | float | [0, ∞) | Segundos desde que este beat entró a `HOLDING`, medidos en el **reloj propio del beat**, que es **EXENTO** del régimen de tiempo de juego (ADR-0002): corre en tiempo real (unscaled) porque **nunca sondea TimeControl** — lee `_process(delta)` directo. Ese "no leer TimeControl" es el mecanismo de exención real; `PROCESS_MODE_ALWAYS` sobre el nodo es defensa complementaria (por si otro sistema introdujera `SceneTree.paused`), no el mecanismo. Si corriera en tiempo de juego se congelaría con la pausa → deadlock (ver Edge Cases) | +| Piso mínimo del beat | `death_hold_min_duration_s` | float (const) | 1.0–4.0 (MVP: **2.0**) | Duración mínima garantizada del beat, sin importar cuán rápido termine la animación | +| Timeout de seguridad | `death_hold_timeout_s` | float (const) | ≥ `min_duration + min_animation_window`, 4.0–15.0 (MVP: **8.0**) | Techo: si `animation_done` nunca llega (bug de animación), el beat se libera igual | +| Ventana mínima de animación | `death_hold_min_animation_window_s` | float (const) | 0.25–2.0 (MVP: **0.5**) | Margen mínimo garantizado entre el piso y el timeout, para que `animation_done` siempre tenga una ventana útil (≫ 1 frame) donde poder cumplirse | +| ¿Animación terminada? | `animation_done` | bool | {0,1} | Señal de Héroes: la resolución V-5→V-6 (silueta → fragmentos de vitral → ícono de reliquia) completó | +| **¿Liberar el beat?** | `release` | bool | {0,1} | Cuando es `true`, se confirma `DEAD` y se entrega el veredicto a Forja de Legado | + +**Output**: booleano evaluado cada tick de la cola mientras hay un beat en `HOLDING`. + +**Constraint enforzado (loud-fail al cargar, coherente con la política del proyecto — Héroes AC-H23, Combate Enforcement)**: no basta la desigualdad estricta `timeout > min` — se exige un **margen mínimo**: `death_hold_timeout_s ≥ death_hold_min_duration_s + death_hold_min_animation_window_s`. Motivo: una desigualdad estricta sola admite configs legales degenerados (p. ej. `min=4.0`, `timeout=4.0001`, ambos dentro de sus rangos individuales) donde la ventana en que `animation_done` puede caer — `[min, timeout)` — es de 0.1ms, menor que un frame a 60fps (16.6ms). Ninguna evaluación por tick podría landear `animation_done=true` dentro de esa ventana, así que **toda** muerte caería silenciosamente al path de timeout, con la config "validada" limpiamente en CI y enmascarando permanentemente el path normal (AC-P04). El margen `death_hold_min_animation_window_s` garantiza una ventana de animación siempre útil (varios frames). Fuera de esta relación → falla ruidosamente al cargar, nunca se clampa. + +**Ejemplos:** +1. **Animación normal** — `min=2.0`, `timeout=8.0`, la animación termina a los 3.1s: en `elapsed=3.1` se cumple `(3.1 ≥ 2.0) AND (animation_done=true)` → `release=true`. Beat de 3.1s. +2. **Animación muy rápida** — la animación reporta `done` a los 1.2s: `(1.2 ≥ 2.0)` es `false` → NO libera; el beat espera al piso. En `elapsed=2.0` → `(2.0 ≥ 2.0) AND (animation_done=true)` → `release=true`. Beat de exactamente 2.0s (el piso protege el ritmo). +3. **Bug de animación (señal nunca llega)** — `animation_done` queda en `false`: el beat corre hasta `elapsed=8.0` → `(8.0 ≥ 2.0) AND (8.0 ≥ 8.0)` → `release=true`, con log de advertencia en build de desarrollo. El juego nunca se cuelga. + +### Nota — no hay fórmula de calidad ni de orden + +- **`relic_quality`**: propiedad de Sistema de Héroes (Fórmula H4), snapshoteada antes de que Permadeath reciba el payload. Permadeath la transporta sin tocarla (Regla 1). No se re-registra. +- **Orden de la cola FIFO**: es una regla determinística (llegada + tiebreak por `hero_id`), no una fórmula numérica — vive en la Sección C, Regla 3. + +## Edge Cases + +- **Si el reloj del beat corriera en la misma escala pausada por `game_time_paused`**: el beat entraría en deadlock — `elapsed_s` nunca avanzaría y `release` jamás se cumpliría. **Resolución**: el nodo de Permadeath que administra el beat corre en un clock exento de la pausa global (en Godot 4.6: `process_mode = PROCESS_MODE_ALWAYS`, `elapsed_s` acumulado por delta unscaled), mientras que Combate/Kaiju **dejan de avanzar su lógica cooperativamente al leer `game_time_paused == true`** (NO por la pausa real del motor — ver Regla 4: Permadeath nunca setea `SceneTree.paused`). La pausa detiene *el mundo*, no *la orquestación del beat*. **Corolario crítico (Regla 4):** la animación de resolución del héroe (V-5→V-6), que alimenta `animation_done`, también debe seguir corriendo durante la pausa (su nodo en `PROCESS_MODE_ALWAYS` o equivalente) — si se congelara, `animation_done` nunca llegaría y todo beat caería al timeout de 8s, enmascarando el caso normal (AC-P04). + +- **Si `animation_done` llega antes del piso `death_hold_min_duration_s`** (animación más rápida que 2.0s): el beat NO se libera todavía — la cláusula `(elapsed_s ≥ min_duration)` lo retiene hasta cumplir el piso. Protege el ritmo (Ejemplo 2 de P1). No es un bug. + +- **Si `animation_done` nunca llega** (bug o asset faltante de Héroes): el timeout `death_hold_timeout_s` libera el beat a los 8.0s con un log de advertencia en build de desarrollo. El juego nunca se cuelga (Ejemplo 3 de P1). **Riesgo de *feel* (recomendado, no bloqueante):** un freeze silencioso de 8s (4× el piso normal) sin ningún progreso visible es indistinguible de un cuelgue — lo que socava el "understatement, no melodrama" justo cuando algo **ya** falló. Candidato de mitigación **no editorializante** (coherente con PU-3): una vez que `elapsed_s` supera `death_hold_min_duration_s` sin `animation_done`, sostener una señal ambiental mínima de "sigue vivo" (p. ej. mantener el lecho de audio dückeado en loop, o un latido de partícula sutil) que **no** comunique ningún resultado ni tier — solo que el momento sigue corriendo, no colgado. La puesta en escena la poseería Héroes/Audio; Permadeath solo expondría el hecho de que está en el tramo post-piso-sin-animación. *Afinar en playtest; decidir junto con Dirección de Audio (sin GDD aún).* + +- **Si `animation_done` y el timeout se cumplen en el mismo tick**: `release` es `true` una sola vez; la confirmación de `DEAD` y el hand-off a Forja de Legado son idempotentes — una entrada `RESOLVED` no se re-procesa (se removió de la cola). + +- **Si 2+ héroes entran a `DYING` en el mismo frame**: cada uno llega con su payload independiente ya snapshoteado por Héroes (AC-H24). Se encolan en orden determinístico (llegada + tiebreak por `hero_id`); solo uno está en `HOLDING` a la vez (satisface Héroes AC-H25). El segundo espera en `QUEUED` sin que su payload congelado cambie mientras espera. + +- **¿Puede un tercer héroe morir mientras ya hay uno en `HOLDING` y otro en `QUEUED`?** Bajo el contrato de la Regla 4, **no**: mientras cualquier beat está en `HOLDING`, `game_time_paused` congela todas las fuentes de daño de tiempo de juego (Combate AC-C52; Kaiju pendiente OQ-P1), así que ningún héroe puede cruzar `health≤0` durante un beat. Una cadena solo se forma con las muertes del **mismo frame** de formación (típicamente un multi-kill de área), y `N` queda fijo ahí (Regla 8). Todas esas entradas se encolan FIFO en ese frame; no se agregan más después. No hay límite de profundidad de cola en el MVP (2-3 héroes por era acota el máximo real a 2-3 entradas). **Caso defensivo (viola el contrato):** si una fuente de daño que **no** respeta `game_time_paused` (p. ej. Kaiju mientras OQ-P1 siga abierto) matara a un héroe durante un `HOLDING`, esa muerte NO se une a la cadena en curso (cuyo `N` ya está fijo) — se trata como una **cadena nueva**, servida cuando la cola actual drene, con su propio presupuesto de cap. Esto acota cada cadena al cap incluso ante una violación del contrato; que ocurra en absoluto es el defecto que AC-P14/OQ-P1 existen para cerrar, no algo que la matemática del cap deba absorber en silencio. + +- **Si llega un payload con campos faltantes o nulos** (`hero_id`, `hero_name`, `relic_category`, o D/T/P): falla ruidosamente al recibirlo en build de desarrollo — Permadeath no puede sellar una muerte ni forjar una reliquia con datos incompletos (coherente con el contrato completo que Héroes AC-H12 garantiza exponer). Nunca sustituye un default silencioso. + +- **Si el juego crashea durante `HOLDING`** (antes de confirmar `DEAD`): el autosave obligatorio ya se disparó en la transición a `DYING` (Héroes AC-H26), *antes* de que Permadeath recibiera el payload — pero ese autosave persiste el héroe como *muerte pendiente de sellar* (`DYING` **con el payload completo, incl. `relic_quality`**, Regla 6), no como `DEAD`. Por eso, al recargar, el flujo de carga **coacciona `DYING → DEAD`** y forja la reliquia desde el payload persistido (Regla 8b): el héroe termina `DEAD` con su reliquia, sin re-reproducir el beat. La muerte es irreversible incluso a través de un crash a mitad del beat (Héroes AC-H27) **y siempre produce al menos una reliquia delgada** (Pilar 2) — la coacción al cargar es lo que hace verdadera esa promesa, que sin ella dejaría al héroe atascado en `DYING` sin reliquia. Permadeath no persiste estado de cola de runtime; depende del payload persistido por el autosave de `DYING`. + +- **Si `game_time_paused` pausa también el temporizador del kaiju** (`kaiju_timer_remaining_ratio` corre en tiempo de juego): la **intención** es que el reloj del clímax se detenga durante cada beat, coherente con la pausa global — no afectaría la reliquia del héroe que muere (su `T` ya está snapshoteado), y para futuras muertes simplemente significaría que los segundos del beat no cuentan contra el clímax. **Pero este es un contrato ABIERTO, no asentado**: el GDD del Temporizador (Approved) solo define su pausa vía el menú de pausa global, no vía `game_time_paused`. Requiere confirmarse/añadirse en ese GDD antes de darse por hecho (ver OQ-P4). + +- **Si `death_hold_timeout_s ≤ death_hold_min_duration_s`** (config inválido): falla ruidosamente al cargar (constraint de P1) — nunca se clampa. + +- **Si todos los héroes de la era mueren** (cola drena hasta vaciarse): Permadeath confirma cada `DEAD` en secuencia y notifica a Forja de Legado, pero **no declara derrota** — esa evaluación la posee Encuentro con Kaiju (Regla 7 / hueco OQ-1 ya resuelto en el proyecto). Permadeath solo vacía su cola. + +- **Si Forja de Legado aún no existe** (estado actual del proyecto): el hand-off del `RESOLVED` es un contrato provisional — Permadeath expone `relic_category` + `relic_quality` en un evento/señal que Forja de Legado consumirá cuando se diseñe. Hoy verificable con un mock del consumidor. + +## Dependencies + +**Dependencias hacia arriba (upstream) — lo que este sistema necesita:** + +| Sistema | Tipo | Interfaz | Estado | +|---|---|---|---| +| Sistema de Héroes | Dura (**productor crítico**) | Al entrar a `DYING`, Héroes expone el payload de circunstancia (`hero_id`, `hero_name`, `relic_category`, D/T/P snapshoteados — `relic_quality` derivable vía H4). Además notifica el fin de su animación de resolución (V-5→V-6) que alimenta `animation_done` de P1 | ✅ Approved — contrato definido por Héroes AC-H12 (payload completo) y V-5/V-6 (animación) | +| Combate/Daño | Blanda (indirecta) | No hay llamada directa: Combate ya notificó `DYING` a Héroes, que es quien reenvía a Permadeath. Combate LEE `game_time_paused` de **TimeControl** (ADR-0002), no de Permadeath; Permadeath solo solicita el régimen `PAUSED` | ✅ Approved — AC-C52 lee `game_time_paused` (fuente = TimeControl) | +| Datos de Era/Civilización | Dura (**solo lectura, load-time**) | Al activar cada era (señal era-`LOADED`), Permadeath lee `N_max` = `get_hero_roster_count()` de **esa** era, para la cross-validación de feasibilidad del cap (Regla 8, AC-P08b). Es la **única** lectura cross-system de Permadeath | ✅ Confirmado 2026-08-16 — el schema `EraDefinition` expone `get_hero_roster_count()` (accesor derivado, Regla 1/AC-1b de Datos de Era) y la dependencia está reflejada en su tabla de Dependientes y en `systems-index.md`. Cierra la acción cross-GDD #6/OQ-P6 | + +**Dependientes hacia abajo (downstream) — dependen de este:** + +| Sistema | Tipo | Interfaz | +|---|---|---| +| Forja de Legado | Dura (**consumidor crítico**) | Al `RESOLVED`, recibe el veredicto final (`hero_id`, `hero_name`, `relic_category`, `relic_quality`) para crear la reliquia. *Sin GDD aún — contrato provisional que Forja deberá respetar.* | +| Sistema de Héroes | Dura (bidireccional) | Permadeath invoca la confirmación de `DEAD` (Héroes posee la escritura del estado); Permadeath controla el *timing* del beat que Héroes solo dispara. Héroes AC-H11/AC-H25 dependen de que Permadeath libere el beat y serialice muertes | +| Combate/Daño | Dura (vía TimeControl) | Lee `game_time_paused` de **TimeControl** para pausar cooldowns durante `HOLDING` (AC-C52). Permadeath es la fuente del régimen `PAUSED`, pero el flag lo publica TimeControl (ADR-0002) | +| Encuentro con Kaiju | Dura (vía TimeControl) | Lee `game_time_paused` de **TimeControl** para congelar telegrafíos/ataques del kaiju durante `HOLDING` (Regla 11 + AC-K89, cierra OQ-P1). Fuente del régimen = Permadeath; dueño del flag = TimeControl | +| Input | Dura | Permadeath libera el contexto `DEATH_HOLD` que Héroes activó al entrar a `DYING` — Input permanece suprimido hasta que Permadeath libere el beat | + +**Relación con Guardado/Persistencia (no es dependencia directa):** el autosave obligatorio lo dispara Héroes en `DYING` (AC-H26), *antes* de que Permadeath actúe. Permadeath no llama a Guardado ni persiste estado de cola propio (ver Edge Cases: al recargar, el héroe ya está `DEAD`). + +**Nota de consistencia bidireccional:** +- Héroes lista a Permadeath como "consumidor crítico" downstream (✅ coincide con este upstream). +- Combate lista a Permadeath como downstream "indirecto" vía el payload que Héroes reenvía (✅ coincide) y espera `game_time_paused` (✅ AC-C52). + +**Acciones cross-GDD — ✅ TODAS APLICADAS 2026-08-16 (registro histórico de lo que se pidió; ver OQ-P6 para el resumen de lo que efectivamente se escribió en cada GDD dueño):** + +1. **Héroes AC-H12 — payload debe incluir `relic_quality` computada.** Hoy AC-H12 expone "exactamente `hero_id`, `hero_name`, `relic_category`, y D/T/P snapshoteado" — sin `relic_quality`. La Regla 1 de este GDD exige que Héroes evalúe H4 en el snapshot e incluya `relic_quality` en el payload. Requiere editar Héroes (Approved). *(OQ-P6.)* +2. **Héroes AC-H26/H27 + Guardado — recuperación por crash (coacción `DYING→DEAD` al cargar).** El autosave de `DYING` debe persistir el payload completo como *muerte pendiente de sellar*, y el flujo de carga debe coaccionar `DYING→DEAD` forjando la reliquia (Regla 8b). Requiere editar Héroes y Guardado/Persistencia — **incluye implementar/verificar AC-P18-ext en esas suites** (Permadeath solo conserva AC-P18, arranque de cola vacía). *(OQ-P6.)* +3. **Guardado/Persistencia — dueño del disparo de autosave.** Su tabla de Interactions atribuye el disparo a Permadeath ("antes de liberar el beat `DEATH_HOLD`"); este GDD (Regla 6) y Héroes AC-H26 lo atribuyen a Héroes en `DYING`. Corregir la fila de Guardado. +4. **Encuentro con Kaiju (Approved) — consumo de `game_time_paused`.** No lo menciona; debe congelar su ciclo de intención durante el beat (ver OQ-P1, AC-P14 bloqueado hasta entonces). +5. **Héroes — nodo de animación V-5→V-6 en `PROCESS_MODE_ALWAYS`.** La Regla 4 y las Edge Cases de este GDD exigen que la animación de resolución del héroe siga corriendo durante la pausa cooperativa (si no, `animation_done` nunca llega y todo beat cae al timeout, enmascarando AC-P04). El requisito debe quedar explícito en el contrato de Héroes, no solo implícito aquí. *(OQ-P6.)* **Aclaración 2026-08-21 (ADR-0002 Accepted):** el mecanismo real por el que la animación sigue corriendo es que ese nodo **no sondea TimeControl** (lee tiempo real) — como TimeControl nunca setea `SceneTree.paused`, `PROCESS_MODE_ALWAYS` no es load-bearing sino defensa complementaria. La conducta ("la animación corre durante el beat") no cambia; solo el *porqué*. +6. **Datos de Era/Civilización — exponer conteo de roster de héroes por era (`N_max`), + reflejar la dependencia bidireccional.** La Regla 8 / AC-P08b exigen leer `N_max` (roster máximo de héroes de la era activa) para validar la feasibilidad del cap, pero el schema `EraDefinition` (AC-1 de Datos de Era) no expone un campo de conteo de héroes (`unit_roster` no está tipado como conteo vs. referencias). Requiere: (a) añadir un campo/accesor de conteo de héroes a `EraDefinition`; (b) que Permadeath lea `N_max` sobre la señal era-`LOADED` (no en su `_ready()`), re-validando por era; (c) añadir Permadeath a la tabla de Dependientes de Datos de Era/Civilización; (d) añadir Datos de Era/Civilización a la fila de Permadeath en `systems-index.md` (hoy lista solo "Sistema de Héroes, Combate/Daño"). Cierra la violación de la regla de dependencias bidireccionales. *(OQ-P6.)* + +## Tuning Knobs + +| Knob | Símbolo | Tipo | Default | Rango seguro | Qué se rompe en extremos | +|---|---|---|---|---|---| +| Piso mínimo del beat | `death_hold_min_duration_s` | float | 2.0 | 1.0–4.0 | <1.0: el beat se siente cortado/apurado, se debilita el understatement trágico (Principio C); >4.0: arrastra el ritmo, choca con el "clímax explosivo" del Pilar 3, sobre todo con varias muertes seguidas | +| Timeout de seguridad | `death_hold_timeout_s` | float | 8.0 | 4.0–15.0 (**y ≥ `min_duration + min_animation_window`**) | < `min_duration + min_animation_window`: **loud-fail al cargar** (la ventana de animación quedaría demasiado angosta — hasta sub-frame — y toda muerte caería al timeout, enmascarando el path normal AC-P04); >15.0: un bug de animación cuelga el juego demasiado tiempo antes de auto-liberarse | +| Ventana mínima de animación | `death_hold_min_animation_window_s` | float | 0.5 | 0.25–2.0 | <0.25: la ventana en que `animation_done` puede cumplirse se acerca peligrosamente a un frame, riesgo de que animaciones normales caigan al timeout por jitter; >2.0: fuerza timeouts muy holgados o pisos muy cortos, reduce el margen de tuning útil | +| Cap de bloqueo total por cadena | `death_hold_total_lockout_cap_s` | float | 9.0 | 6.0–14.0 (**y ≥ `min_duration`, y ≥ `N_max × min_compressed`**) | <6.0 o < `N_max × min_compressed`: **loud-fail al cargar** (el cap sería infeasible — ni con todos los beats en el piso comprimido la cadena cabría, rompiendo la promesa de AC-P21); >14.0: el peor caso de apilamiento vuelve a estancar el clímax explosivo (Pilar 3). Se afina en playtest | +| Piso comprimido bajo el cap | `death_hold_min_compressed_s` | float | 1.2 | 1.0–`min_duration` | Piso reducido para beats de una cadena que se acerca al cap (Regla 8). <1.0: el beat comprimido se siente cortado; ≥`min_duration`: no comprime nada, el cap no puede cumplirse con cadenas largas | + +**Interacciones entre knobs (enforzadas, no solo recomendadas — todas validadas al cargar con loud-fail, coherente con Héroes AC-H23 / Combate Enforcement; nunca se clampa ni se sustituye en silencio):** +- **Margen de release (P1):** `death_hold_timeout_s ≥ death_hold_min_duration_s + death_hold_min_animation_window_s`. La desigualdad estricta sola no basta: sin margen mínimo, la ventana `[min, timeout)` puede volverse sub-frame y toda muerte caería al timeout con la config "validada" (ver constraint de P1 y AC-P08). +- **Feasibilidad del cap (Regla 8):** `death_hold_total_lockout_cap_s ≥ death_hold_min_duration_s` **y** `death_hold_total_lockout_cap_s ≥ N_max × death_hold_min_compressed_s`, con `N_max` leído al cargar desde Datos de Era/Civilización (roster máximo por era). Sin esta validación, un cap legal-por-rango podía ser matemáticamente infeasible (p. ej. `cap=6.0`, `min_compressed=1.2`, `N_max=6` → `6.0 < 7.2`), rompiendo la garantía de AC-P21. +- **Piso comprimido:** `death_hold_min_compressed_s ≤ death_hold_min_duration_s` (un "piso comprimido" mayor que el normal no comprimiría nada). + +**Nota de alcance**: Permadeath tiene deliberadamente **pocos knobs** — es un sistema de orquestación, no de balance. La calidad de la reliquia (`relic_quality`, con sus knobs `base_accidental`/`base_deliberate`/`w_stakes`/`w_purpose`) la posee Sistema de Héroes; Permadeath no duplica ninguno. Los dos knobs de acá son puramente de *feel del ritmo del beat*, no de balance de juego. + +**Knobs que NO existen (decisiones conscientes):** +- **No hay knob de orden de cola**: la serialización es FIFO determinística fija (Regla 3), no configurable — evitar que el orden de resolución de muertes se vuelva superficie de tuning protege el anti-melodrama (no se editorializa qué muerte "importa más"). +- **No hay knob de profundidad máxima de cola**: acotada de facto por 2-3 héroes/era; añadir un límite configurable sería complejidad sin beneficio en el MVP. +- **No hay knob de pausa on/off**: `game_time_paused` durante `HOLDING` es una regla dura (Regla 4), no un toggle — desactivarla reintroduciría el daño inevitable e injusto que la pausa existe para prevenir. +- **No hay knob de skip/aceleración en reintentos (tradeoff consciente)**: un jugador que reintenta una era fallida difícil come el mismo beat obligatorio (hasta ~9s con cadena topada al cap) en cada intento, sin camino de aceleración. Esto es tensión real entre "irreversible e ininterrumpible por diseño" (correcto para el Pilar 1) y "el mismo ritual forzado en cada reintento puede generar fastidio en vez de reverencia". **Decisión (2026-08-14): se mantiene rígido** — el peso del sacrificio no debe erosionarse por conveniencia de repetición, y el cap de la Regla 8 ya acota el peor caso. Se re-evalúa en playtest si los datos muestran fatiga en reintentos (candidato a un ajuste de *feel*, no a un skip que reintroduzca agencia sobre el momento). **Métrica de habituación a vigilar (distinta del fastidio genérico):** el riesgo específico de repetir un beat idéntico entre reintentos es la *habituación* — que el jugador deje de vivirlo como ritual y lo lea como obstáculo. Señal concreta a instrumentar: intentos de input suprimidos durante `DEATH_HOLD` **correlacionados con el número de reintento** de la misma era (p. ej. mashing de skip/pausa que sube con cada reintento), y/o tiempo hasta el primer input tras liberarse el beat en reintentos tardíos. No confundir con volumen de quejas genérico; la habituación se ve en la curva por-reintento, no en el promedio. + +**Constantes consumidas (no se duplican)**: una sola — `N_max` (roster máximo de héroes **de la era activa**) leído de Datos de Era/Civilización para la cross-validación de feasibilidad del cap (Regla 8). Es la **única** lectura cross-system de Permadeath, justificada por esa invariante. Como el roster varía por civilización/era, se lee **al activarse cada era** (sobre la señal era-`LOADED`, no en el `_ready()` de Permadeath — evita la carrera contra la máquina `LOADING→LOADED`), y la validación del cap se re-ejecuta por era. Permadeath no lee ninguna constante de balance ni recibe nada más que el payload ya calculado de Héroes. *(⚠️ Contrato provisional: el schema `EraDefinition` no expone hoy un campo de conteo de héroes; añadirlo es acción cross-GDD #6 / OQ-P6. Hasta entonces, la validación es testeable inyectando `N_max` como parámetro — ver AC-P08b.)* + +## Visual/Audio Requirements + +*Referencia rectora: Art Bible "What the Gods Left Behind" Secciones 2.3 (Muerte/Sacrificio, Principio C). **Permadeath no diseña el arte del beat** — lo poseen Sistema de Héroes (V-5/V-6: luz clave, desaturación, silueta → fragmentos de vitral → ícono de reliquia) y su audio (A-1/A-2/A-5). Esta sección fija únicamente el **contrato de timing/sincronización** que Permadeath provee.* + +**PV-1 — Permadeath posee el reloj, no la puesta en escena.** Permadeath controla *cuándo* empieza y termina el beat (Fórmula P1); Héroes controla *qué se ve/oye*. El hand-off es la señal `animation_done` (Héroes → Permadeath) más los hit-points visuales/sonoros que Héroes sincroniza contra la duración del beat que Permadeath expone. + +**PV-2 — El piso mínimo garantiza que el arte tenga tiempo de respirar (relativo al piso *efectivo*).** `death_hold_min_duration_s=2.0` asegura que la composición del Principio C (pausa deliberada, sin corte a negro) nunca se sienta cortada aunque la animación reporte `done` antes. Es un contrato de *feel* con Héroes V-5. **Precisión bajo compresión (Regla 8):** cuando una cadena de muertes apiladas dispara la compresión, el piso *efectivo* de cada beat baja hasta `death_hold_min_compressed_s` (default 1.2s) — por debajo del piso normal de 2.0s. La garantía de "respirar" de PV-2 es, por tanto, **relativa al piso efectivo vigente**, no absoluta: en una cadena comprimida el arte respira menos, por diseño (el cap del Pilar 3 gana sobre la dignidad del beat en ese peor caso raro; ver Regla 8 y la decisión 2026-08-14). `death_hold_min_compressed_s` es en sí un contrato de *feel* con Héroes V-5: su cota inferior (1.0) existe para que ni el beat más comprimido colapse por completo la composición. El riesgo de que la cadena comprimida se lea como "cola de UI" en vez de "muertes presenciadas" es un ítem de vigilancia de playtest explícito (ver métrica en Regla 8). + +**PV-3 — La pausa global refuerza el aislamiento del Principio C.** `game_time_paused` congela el combate circundante durante el beat, lo que visual y sonoramente colabora con la desaturación/ducking que Héroes A-1 pide ("el mundo se aparta"). No es una decisión estética de Permadeath — es un efecto colateral funcional de la pausa (Regla 4) que Héroes puede aprovechar. + +**PV-4 — Audio de la pausa: handoff, no verbo propio.** Coherente con Combate CA-3 y Héroes A-1: al iniciar `HOLDING`, el lecho de combate se ducke; el audio del beat lo posee Héroes/Permadeath-timing, no este sistema como diseñador de sonido. *(Contrato provisional — sin Dirección de Audio aún.)* + +**PV-5 — La reliquia siempre se revela, incluso en el path de timeout.** Cuando el beat se libera por timeout (`animation_done` nunca llegó, Ejemplo 3 de P1), la animación V-5→V-6 pudo haberse perdido o quedado a medias — pero un `RESOLVED` **nunca** puede ocurrir sin que el ícono de reliquia (V-6) se muestre en su estado final. El contrato de timing de Permadeath incluye señalar a Héroes/Forja que, en el path de timeout, la revelación de reliquia debe **forzarse a su end-state** (snap al ícono final), no omitirse. Sin esto, un bug de animación produciría un `DEAD` + forja sin ninguna confirmación visible para el jugador, rompiendo el payoff del Pilar 2 ("el pasado es poder" — la reliquia debe *verse* nacer). *(La puesta en escena del snap final la posee Héroes V-6; Permadeath solo garantiza que ocurra. Ver AC-P06b.)* + +## UI Requirements + +*Permadeath **no tiene HUD propio**. Esta sección fija su única frontera con la UI: qué se congela y qué se suprime durante el beat.* + +**PU-1 — Freeze del readout de veredicto durante `HOLDING`.** Al iniciar el beat, el readout de `relic_quality` proyectada de Héroes (U-1..U-4) ya se congeló en el valor snapshoteado (Héroes AC-H32c/U-4) — Permadeath no lo actualiza ni lo re-dibuja; solo mantiene el estado `HOLDING` mientras dura. La resolución visual final (readout → ícono de reliquia, Héroes V-6) ocurre al liberar el beat. + +**PU-2 — Input suprimido, sin UI de interrupción, sin pausa real.** Durante `HOLDING` el Input está en `DEATH_HOLD` (contrato de Héroes/Input) — Permadeath no expone ningún control de "saltar"/"cancelar" el beat, **ni permite abrir el menú de pausa global** (su acción queda suprimida con el resto del Input; ver Regla 4 y AC-P07c). No hay UI de skip: el beat es inevitable por diseño (Pilar 1). Prohibido cualquier prompt de "presiona para continuar" que reintroduzca agencia sobre un momento que debe ser presenciado. La imposibilidad de pausar el motor durante el beat es lo que garantiza que ningún `RESOLVED` ocurra fuera de la vista del jugador. + +**PU-3 — Sin editorializar (understatement).** Coherente con Héroes U-5 y Combate CU-4: Permadeath no dispara ningún texto de veredicto ("Reliquia forjada", "Muerte confirmada") — la única comunicación es la diegética que Héroes V-6 ya define. Permadeath solo orquesta el timing. + +## Acceptance Criteria + +*Convención: formato Dado/Cuando/Entonces + etiqueta de tipo de evidencia (`[Logic/unit]`, `[Integration]`). Los AC marcados **(bloqueado)** dependen de un sistema sin GDD (Forja de Legado) — hoy verificables solo con mock en el límite de este sistema. (Actualización 2026-08-16: los bloqueos por contrato-no-reflejado contra Héroes, Encuentro con Kaiju y Datos de Era se resolvieron en la pasada de reconciliación cross-GDD — ver OQ-P6. AC-P01, AC-P08c y AC-P14 ya no llevan el marcador `(bloqueado)`; quedan pendientes solo de implementación, como cualquier AC de Integration en esta fase del proyecto. AC-P06b sigue bloqueado por una razón distinta y sin resolver — OQ-P2, la forma exacta de la señal `animation_done`/hook de revelación forzada.)* + +### A. Recepción y transporte del payload (Reglas 1, 5) + +**AC-P01 — Recepción de payload completo desde Héroes** **[Integration]** +*Contrato confirmado, 2026-08-16: Héroes AC-H12 ya expone `relic_quality` en el payload de `DYING` — ver `design/gdd/sistema-de-heroes.md`. Ya no depende de un Mock Contract Assumption; pendiente solo de implementación.* +Dado un héroe que Héroes transiciona a `DYING` exponiendo `hero_id`, `hero_name`, `relic_category`, **`relic_quality` ya computada (vía Fórmula H4 de Héroes)** y los D/T/P snapshoteados que la acompañan, cuando Permadeath recibe el payload, entonces crea una entrada `QUEUED` con exactamente esos valores, sin mutar ninguno. *(El fixture del payload incluye `relic_quality` — es el mismo conjunto de campos que asumen AC-P02/AC-P11/AC-P16.)* + +**AC-P02 — Permadeath no computa ni recalcula `relic_quality` ni D/T/P** **[Logic/unit]** +*El fixture de payload inyecta `relic_quality` directamente (unit-testeable con o sin Héroes real, mismo patrón que AC-P08b). Contrato confirmado contra Héroes AC-H12 real, 2026-08-16.* +Dado un payload con `relic_quality` ya computada y D/T/P snapshoteados, cuando Permadeath procesa la muerte de principio a fin, entonces en ningún punto computa ni recomputa `relic_quality` ni reescribe D/T/P — el veredicto entregado a Forja de Legado deriva byte-por-byte del `relic_quality` recibido en el payload. + +**AC-P03 — Loud-fail con payload incompleto** **[Logic/unit]** +Dado un payload con cualquier campo faltante o nulo (`hero_id`, `hero_name`, `relic_category`, o D/T/P), cuando Permadeath lo recibe, entonces falla ruidosamente en build de desarrollo — nunca sustituye un default silencioso ni sella una muerte con datos incompletos. + +### B. Timing del beat (Fórmula P1, Regla 2) + +**AC-P04 — Release normal: piso + animación** **[Logic/unit]** +Dado `death_hold_min_duration_s=2.0`, `death_hold_timeout_s=8.0` y `animation_done` llegando a `elapsed_s=3.1`, cuando se evalúa P1, entonces `release=true` en `elapsed_s=3.1` (beat de 3.1s). + +**AC-P05 — El piso retiene una animación demasiado rápida** **[Logic/unit]** +Dado `animation_done=true` en `elapsed_s=1.2` con `min_duration=2.0`, cuando se evalúa P1, entonces `release=false` en 1.2 y `release=true` exactamente en `elapsed_s=2.0` (el piso protege el ritmo). + +**AC-P06 — El timeout libera si la animación nunca llega** **[Logic/unit]** +Dado `animation_done=false` durante todo el beat con `timeout=8.0`, cuando `elapsed_s` alcanza 8.0, entonces `release=true` y se emite un log de advertencia en build de desarrollo — el beat nunca cuelga. + +**AC-P06b — La reliquia se revela incluso en el path de timeout** **(bloqueado — el hook `force_relic_reveal_final()` no está confirmado en el contrato real de Héroes V-6; ver PV-5, OQ-P2)** **[Integration]** +Dado un beat liberado por timeout (`animation_done` nunca llegó), cuando Permadeath alcanza `RESOLVED`, entonces la señal de "forzar revelación a end-state" (V-6) se emite a Héroes/Forja **antes o junto** al hand-off — ningún `RESOLVED` ocurre sin que el ícono de reliquia se muestre en su estado final. *(Mock Contract Assumption: el mock de Héroes/V-6 expone un hook `force_relic_reveal_final()`; re-verificar contra el contrato real de la animación V-6 de Héroes — ver PV-5, OQ-P2.)* + +**AC-P07 — Reloj del beat exento de la pausa global (no deadlock), con aislamiento probado** **[Integration]** +Dado un beat en `HOLDING` con `game_time_paused=true`, cuando transcurre tiempo, entonces (a) `elapsed_s` del beat sigue avanzando (clock unscaled/`PROCESS_MODE_ALWAYS`) y el beat progresa hasta `release`, **y (b) en el mismo test, el reloj de al menos un sistema consumidor (p. ej. un cooldown de Combate, cross-check con AC-P13) NO avanza** — las dos aserciones juntas prueban el aislamiento (el reloj del beat corre exento; el mundo cooperativamente se detiene). La aserción (b) es la que convierte esto en prueba de aislamiento y no solo de progreso; AC-P13 la cubre en detalle para Combate. + +**AC-P07b — Pausa cooperativa, no `SceneTree.paused`** **[Integration]** +Dado un beat que entra a `HOLDING`, cuando se inspecciona el estado del árbol de escena, entonces `SceneTree.paused` permanece en `false` — Permadeath nunca lo setea (confirma el mecanismo cooperativo de la Regla 4). *(La detención del mundo por lectura de `game_time_paused` la cubre AC-P07/AC-P13; la no-congelación del nodo de animación la cubre AC-P04 al probar que `animation_done` llega antes del timeout en el caso normal — este AC solo aísla la aserción sobre `SceneTree.paused`.)* + +**AC-P07c — El beat nunca se resuelve bajo pausa real del motor** **[Integration]** +Dado un beat en `HOLDING`, cuando se intenta disparar la acción de menú de pausa global, entonces la acción queda suprimida (no se setea `SceneTree.paused=true`) y el beat continúa; y como invariante complementario, en ningún tick donde `SceneTree.paused==true` (si se llegara por cualquier vía) Permadeath confirma `DEAD` ni hace hand-off a Forja — ningún `RESOLVED` ocurre con el árbol en pausa real (Regla 4: garantiza que la muerte nunca se sella fuera de la vista del jugador). + +**AC-P08 — Constraint de config: margen mínimo `timeout ≥ min + min_animation_window` loud-fail** **[Logic/unit]** +Dado un config con `death_hold_timeout_s < death_hold_min_duration_s + death_hold_min_animation_window_s` (incluye el caso degenerado `min=4.0, timeout=4.0001` que satisface la desigualdad estricta pero deja una ventana de animación sub-frame), cuando se carga, entonces falla ruidosamente — nunca se clampa. *(La desigualdad estricta sola NO basta: este AC prueba el margen, no solo `timeout > min`.)* + +**AC-P08b — Cross-validación de feasibilidad del cap loud-fail** **[Logic/unit]** +Dado `N_max` **inyectado directamente como parámetro** a la función de validación (desacoplado de la lectura real de Datos de Era, que aún no expone el campo — ver nota) y un config con `death_hold_total_lockout_cap_s < N_max × death_hold_min_compressed_s` (p. ej. `cap=6.0, min_compressed=1.2, N_max=6` → `6.0 < 7.2`) o `cap < death_hold_min_duration_s`, cuando se valida, entonces falla ruidosamente — nunca se clampa el cap en silencio. Garantiza que la promesa de AC-P21 ("nunca excede el cap") sea siempre satisfacible. *(La función de validación es unit-testeable hoy con `N_max` como parámetro; el **cableado end-to-end** — que Permadeath lea `N_max` real de Datos de Era sobre la señal era-`LOADED` — está bloqueado hasta que Datos de Era exponga el campo de conteo de roster de héroes: acción cross-GDD #6 / OQ-P6. Ese cableado se verifica como Integration cuando el campo exista; ver AC-P08c.)* + +**AC-P08c — Lectura real de `N_max` desde Datos de Era, por era** **[Integration]** +*Contrato confirmado, 2026-08-16: Datos de Era expone `get_hero_roster_count()` (accesor derivado, no campo autoral separado — ver `design/gdd/datos-de-era-civilizacion.md` Regla 1/AC-1b) y emite la señal era-`LOADED`. Ya no depende de un Mock Contract Assumption; pendiente solo de implementación.* +Dado una era que alcanza `LOADED` con un roster de héroes de tamaño conocido, cuando Permadeath recibe la señal era-`LOADED`, entonces lee `N_max` de esa era vía `get_hero_roster_count()` (no en su propio `_ready()`, evitando la carrera contra `LOADING→LOADED`) y re-ejecuta la validación de feasibilidad del cap (AC-P08b) contra ese `N_max` — confirmando la cardinalidad por-era (una era con roster distinto re-valida con su propio `N_max`). + +### C. Cola FIFO y serialización (Regla 3) + +**AC-P09 — Un solo beat `HOLDING` a la vez** **[Integration]** +Dado dos o más entradas en la cola, cuando se procesan, entonces exactamente una está en `HOLDING` en cualquier instante; las demás permanecen en `QUEUED` (satisface Héroes AC-H25: nunca dos `DEATH_HOLD` sostenidos simultáneos). + +**AC-P10 — Orden FIFO determinístico con tiebreak por `hero_id`** **[Logic/unit]** +Dado dos héroes que entran a `DYING` en el mismo frame con `hero_id` distintos, cuando se encolan, entonces el orden de resolución es determinístico (llegada, y ante empate exacto el menor `hero_id` primero) — reproducible en cada corrida. + +**AC-P11 — El payload en `QUEUED` no cambia mientras espera** **[Integration]** +Dado un héroe en `QUEUED` detrás de otro en `HOLDING`, cuando el primer beat se resuelve y transcurre tiempo, entonces el payload congelado del segundo (D/T/P, `relic_quality`) es idéntico al del momento en que se encoló. + +### D. Pausa global (Regla 4) + +**AC-P12 — `game_time_paused` activo durante `HOLDING`** **[Integration]** +Dado un beat que entra a `HOLDING`, cuando Permadeath solicita el régimen `PAUSED` a TimeControl (ADR-0002) y se inspecciona `TimeControl.game_time_paused`, entonces es `true`; y al liberarse el beat (`RESOLVED`) Permadeath llama `release_regime`, y el flag vuelve a `false` (asumiendo ninguna otra fuente PAUSED activa). *Fuente del flag = TimeControl; Permadeath es el solicitante.* + +**AC-P13 — Cooldowns de Combate se detienen durante el beat** **(verificable — Combate Approved, AC-C52)** **[Integration]** +Dado un beat en `HOLDING` con `game_time_paused=true`, cuando transcurre tiempo, entonces ningún cooldown de Combate avanza (cross-check con Combate AC-C52) — el mundo de combate está congelado. + +**AC-P14 — El kaiju no ataca ni telegrafía durante el beat** **[Integration]** +*Contrato confirmado, 2026-08-16: Encuentro con Kaiju consume `game_time_paused` vía su nueva Regla 11 + AC-K89 — ver `design/gdd/encuentro-con-kaiju.md`. Ya no depende de un Mock Contract Assumption; pendiente solo de implementación. Cierra OQ-P1.* +Dado un beat en `HOLDING`, cuando transcurre tiempo, entonces el kaiju no avanza su ciclo de intención ni aplica daño. + +### E. Confirmación de `DEAD`, hand-off e irreversibilidad (Reglas 1, 5, 6) + +**AC-P15 — Confirmación de `DEAD` vía Héroes, no escritura directa** **[Integration]** +Dado un beat que alcanza `release`, cuando Permadeath confirma la muerte, entonces invoca el método de confirmación de Héroes (que posee la escritura del estado) — Permadeath nunca escribe directamente el campo de estado del héroe. + +**AC-P16 — Hand-off del veredicto a Forja de Legado, una sola vez** **(bloqueado — Forja de Legado sin GDD)** **[Integration]** +*Mock Contract Assumption: el mock de Forja expone un único hook `receive_relic(hero_id, hero_name, relic_category, relic_quality)`. Re-verificar contra el GDD real de Forja de Legado.* +Dado un beat que alcanza `RESOLVED`, cuando Permadeath entrega el veredicto, entonces Forja de Legado (o su mock) recibe `hero_id`, `hero_name`, `relic_category`, `relic_quality` exactamente una vez — el hand-off es idempotente, sin doble entrega. + +**AC-P17 — Release idempotente ante animación + timeout en el mismo tick** **[Logic/unit]** +Dado un tick donde `animation_done=true` y `elapsed_s ≥ timeout` se cumplen a la vez, cuando se evalúa `release`, entonces el método de confirmación de `DEAD` de Héroes (spy) se invoca **exactamente una vez** **Y** el hand-off a Forja (mock `receive_relic`) se invoca **exactamente una vez** — observables concretos de la idempotencia, no una re-afirmación de "se resuelve una vez". *(Mecanismo de exactamente-una-vez: la entrada se remueve de la cola **antes** de invocar la confirmación y el hand-off, de modo que un re-tick sobre la misma entrada no la encuentra — ver Regla 5; AC-P17b prueba ese no-op.)* + +**AC-P17b — Re-evaluar una entrada ya resuelta es no-op** **[Logic/unit]** +Dado una entrada que ya alcanzó `RESOLVED` y fue removida de la cola, cuando se invoca `_evaluate_release()` (o el hook equivalente) una segunda vez con ese `entry_id`, entonces no ocurre ninguna acción — ni confirmación de `DEAD` ni hand-off adicional — porque la entrada ya no está en la cola. Da una superficie de API concreta a la garantía "no se re-procesa" que AC-P17 asume. + +**AC-P18 — Permadeath no reanuda ni re-reproduce beats tras recargar** **[Integration]** +Dado un reinicio tras un crash ocurrido durante un `HOLDING` (autosave de `DYING` con payload persistido, Regla 6), cuando Permadeath se inicializa, entonces **no** encuentra ningún estado `HOLDING`/`QUEUED` restaurado, **no** reanuda ni re-reproduce ningún beat pendiente, y **no** emite ninguna llamada de resume — su cola de runtime arranca vacía (el beat es una experiencia de sesión, no un estado a restaurar; Regla 8b). *Este AC cubre solo lo que Permadeath posee. La coacción `DYING→DEAD` + forja desde la `relic_quality` persistida vive **enteramente** en el flujo de carga de Héroes+Guardado y se testea allí (ver AC reubicado en Héroes/Guardado, OQ-P6) — Permadeath no expone API para dispararla ni observarla.* + +**AC-P18-ext (cross-GDD, propiedad de Héroes+Guardado) — coacción a `DEAD` + reliquia garantizada al cargar.** +Dado un héroe persistido como *muerte pendiente de sellar* (`DYING` + payload completo, incl. `relic_quality`), cuando se recarga, entonces el flujo de carga lo coacciona a `DEAD` (nunca `ALIVE`/`DYING` persistente) y forja su reliquia desde la `relic_quality` persistida. *Este AC se implementa y verifica en las suites de Héroes/Guardado, no en la de Permadeath — listado aquí solo para trazabilidad del contrato (OQ-P6).* + +**AC-P19 — Permadeath no duplica el autosave** **[Logic/unit]** +Dado el flujo completo de una muerte, cuando Permadeath la procesa, entonces no invoca a Guardado/Persistencia — el único autosave lo disparó Héroes en `DYING` (AC-H26). + +**AC-P20 — Permadeath no declara victoria/derrota** **[Logic/unit]** +Dado que la cola drena hasta vaciarse (todos los héroes muertos), cuando la última muerte se resuelve, entonces Permadeath no emite ninguna señal de victoria/derrota de era — solo notifica cada `DEAD` a Forja de Legado (la evaluación la posee Encuentro con Kaiju). + +### F. Ritmo de muertes apiladas y presentación en cola (Reglas 3, 8) + +**AC-P21 — Cap de bloqueo total: compresión uniforme sobre `N` fijo, cap nunca excedido** **[Logic/unit]** +Dado una cadena de `N` muertes (con `N` **fijado en el frame de formación**, Regla 8) encoladas con `death_hold_min_duration_s`, `death_hold_total_lockout_cap_s`, `death_hold_min_compressed_s` y `death_hold_min_animation_window_s` configurados, cuando `N × death_hold_min_duration_s` excedería el cap, entonces **todos** los beats de la cadena usan el mismo piso `clamp(cap / N, min_compressed, min_duration)` y la suma de pisos **nunca excede `death_hold_total_lockout_cap_s`**. Caso concreto de regresión (el config que antes rompía el cap): `min_duration=4.0, cap=6.0, min_compressed=1.2, N=3` → piso uniforme `clamp(6.0/3, 1.2, 4.0)=2.0`, total `3×2.0=6.0 = cap` (antes, con compresión reactiva, daba `4.0+1.2+1.2=6.4 > cap`). Cada beat sigue reproduciéndose (nunca se salta, respeta `animation_done`/timeout por encima de su piso). **Alcance (ver Regla 8, "Alcance de la garantía del cap"):** esta garantía cubre la suma de *pisos* — el camino diseñado donde todo beat de la cadena resuelve vía `animation_done`. Si uno o más beats caen al path de timeout (bug de animación), el total puede exceder el cap; ese caso queda fuera del alcance de este AC y es la misma tolerancia a bugs ya aceptada por AC-P06 para un beat individual, no una regresión de este AC. + +**AC-P21c — `N` no se recomputa a mitad de cadena (regresión del arribo escalonado)** **[Logic/unit]** +Dado una cadena con `N` ya fijado y un primer beat en `HOLDING`, cuando el test inyecta una señal de muerte adicional **después** de que el primer beat arrancó (simulando el arribo escalonado que la Regla 4 debería impedir), entonces (a) el `N` de la cadena en curso **no** cambia y ningún piso ya computado se altera, y (b) la muerte inyectada se trata como una **cadena nueva** (servida tras drenar la actual, con su propio presupuesto de cap) — nunca se pliega a la cadena en curso subiendo su `N`. Regresión directa del config que rompía el cap por recompute escalonado (`min_duration=2.0, cap=9.0, min_compressed=1.2`, arribos 1→…→5: con recompute daba `2.0 + 4×1.8 = 9.2 > cap`; con `N` fijo por cadena, la suma de cada cadena respeta su propio cap). + +**AC-P21b — Compresión sin favoritismo de orden ni `hero_id`** **[Logic/unit]** +Dado una cadena de `N` muertes que dispara compresión, cuando se comparan los pisos asignados, entonces **todos los beats reciben el mismo piso** — ni el primero, ni el último, ni el de menor/mayor `hero_id` recibe un beat más largo o más corto que los demás (confirma la compresión uniforme de la Regla 8; descarta la asimetría de duración por `hero_id` que la compresión reactiva introducía). + +**AC-P22 — Héroe en `QUEUED` no reproduce su colapso hasta `HOLDING`** **[Integration]** +Dado un héroe en `QUEUED` detrás de otro en `HOLDING`, cuando se inspecciona su presentación, entonces se sostiene en el umbral de colapso (no ha iniciado V-5) y su beat presenciado (V-5→V-6) comienza **solo** al entrar a `HOLDING` — ninguna muerte se colapsa fuera de campo mientras el jugador presencia otra (Regla 3, Player Fantasy del testigo). + +### Clasificación de tipo de story (gate de evidencia) + +| AC # | Tipo | Evidencia | Gate | +|---|---|---|---| +| AC-P02, P03, P04, P05, P06, P08, P08b, P09, P10, P11, P17, P17b, P19, P20, P21, P21b, P21c | Logic | Unit test (`tests/unit/permadeath/`) | Bloqueante | +| AC-P01, P07, P07b, P07c, P08c, P12, P13, P14, P15, P18, P22 | Integration | Integration test | Bloqueante | +| AC-P06b, P16 | Integration (bloqueado) | Integration test con mock (hook `force_relic_reveal_final()` de Héroes V-6 / Forja de Legado) | Bloqueante (hoy solo vía mock) | + +*(Nota: AC-P09 y AC-P11 se reclasificaron de Integration a Logic/unit — prueban la máquina de estados de la cola de Permadeath con payloads inyectados, sin llamada cross-system real. AC-P18 se estrechó a lo que Permadeath posee (arranque de cola vacía, sin resume) y ahora es Integration puramente ownable; la coacción `DYING→DEAD` al cargar se movió a AC-P18-ext, que se implementa/verifica en las suites de Héroes+Guardado, no aquí — ver OQ-P6. **Actualización 2026-08-16:** AC-P01, AC-P08c y AC-P14 se reclasificaron de vuelta a Integration normal — la pasada de reconciliación cross-GDD (OQ-P6) confirmó sus contratos reales contra Héroes AC-H12, Datos de Era `get_hero_roster_count()`/AC-1b, y Encuentro con Kaiju Regla 11/AC-K89 respectivamente; ya no dependen de un Mock Contract Assumption, solo de implementación. AC-P06b permanece Integration (bloqueado) — su hook `force_relic_reveal_final()` depende de OQ-P2 (forma exacta de la señal de Héroes), que sigue sin resolver. AC-P02 permanece Logic/unit — testeable hoy inyectando el fixture de payload directamente, mismo patrón que AC-P08b, ahora confirmado contra el contrato real.)* + +## Open Questions + +**OQ-P1 — ✅ RESUELTO 2026-08-16 — Contrato `game_time_paused` ya está en Encuentro con Kaiju.** +Permadeath introduce `game_time_paused` (Regla 4) que Combate ya espera (AC-C52) y que Encuentro con Kaiju ahora también consume vía su nueva Regla 11 + AC-K89 (`design/gdd/encuentro-con-kaiju.md`, parche 2026-08-16). AC-P14 ya no está bloqueado. **Riesgo abierto residual (godot-specialist):** el `process_mode` real de los nodos de Combate/Kaiju no está confirmado por un spike de motor real — el modelo cooperativo asume que sus `_process` corren para poder leer el flag; si algún nodo dependiera del menú de pausa real del motor podría colisionar con este modelo. No asumirlo resuelto solo por el texto del GDD; verificar al integrar (parte del spike de OQ-P5, que sigue pendiente). + +**OQ-P2 — Forma exacta de la señal `animation_done` desde Héroes.** +La Fórmula P1 consume `animation_done` como la notificación de fin de la animación de resolución de Héroes (V-5→V-6). Héroes describe esa animación (V-5/V-6) pero no formaliza una señal de "animación terminada" que Permadeath pueda escuchar. **Acción**: confirmar la interfaz exacta (señal Godot con/sin payload, callback) — contrato bidireccional con Héroes. Hoy AC-P04/P05 usan un mock del disparo. *Resolver al implementar el beat.* + +**OQ-P3 — Contrato de hand-off con Forja de Legado (sin GDD).** +Al `RESOLVED`, Permadeath entrega `hero_id`, `hero_name`, `relic_category`, `relic_quality` a Forja de Legado (#11, sin GDD aún). La forma exacta (señal, método, estructura del evento) es provisional. **Acción**: confirmar al diseñar Forja de Legado (AC-P16 bloqueado hasta entonces). Es el consumidor crítico downstream — la otra mitad de la cadena de legado. + +**OQ-P4 — ✅ RESUELTO 2026-08-21 — ADR-0002 (Accepted).** +La pregunta era si la pausa de juego alcanza al Temporizador de Preparación/Ritual. **ADR-0002 la resolvió** introduciendo el servicio Core **TimeControl** como dueño único del régimen de tiempo de juego: el Temporizador **consume `TimeControl.game_tick`/`time_scale`** para avanzar su `elapsed_s`, de modo que se congela con `PAUSED` y se ralentiza con `SLOWED` "gratis", sin que Permadeath tenga que cablearlo directamente. La distinción que preocupaba (el consumidor debe *dejar de avanzar*, no solo "leer la escala") se satisface porque el reloj del Temporizador ahora deriva de `game_tick`, que no avanza en pausa. El menú de pausa global del Temporizador (AC-T22, `SceneTree.paused`) es un régimen distinto y ortogonal — ver la nota de reconciliación en `temporizador-de-preparacion-ritual.md`. Ver ADR-0002 §GDD Requirements Addressed. + +**OQ-P6 — ✅ RESUELTO 2026-08-16 — Pasada de reconciliación de contratos cross-GDD completa.** +Los 6 ítems se aplicaron directamente a los GDDs dueños (edición manual colaborativa — `/propagate-design-change` no aplicaba: sin ADRs aún, sin historial de git para diffear; ver sesión de reconciliación 2026-08-16): +1. ✅ Héroes AC-H12 ahora expone `relic_quality` en el payload de `DYING` (evaluada vía H4 en el frame del snapshot). +2. ✅ Héroes AC-H26 ahora exige el payload completo marcado *pendiente de sellar*; nuevo AC-H27c implementa la coacción `DYING→DEAD` + forja de reliquia al recargar (AC-P18-ext vive ahí, no aquí). +3. ✅ Guardado/Persistencia corregido en 6 lugares — Sistema de Héroes (no Permadeath) es quien dispara el autosave, en `DYING`. +4. ✅ Encuentro con Kaiju consume `game_time_paused` vía su nueva Regla 11 + AC-K89 — cierra OQ-P1. +5. ✅ Héroes documenta el requisito `PROCESS_MODE_ALWAYS` para el nodo de animación V-5→V-6 (y sus nodos hijos de partícula/audio/tween). +6. ✅ Datos de Era/Civilización expone `get_hero_roster_count()` (accesor derivado, AC-1b) y refleja la dependencia bidireccional en su propia tabla de Dependientes — desbloquea AC-P08c. + +Todas las ACs de Permadeath que dependían de estos contratos (AC-P01, AC-P08c, AC-P14) se reclasificaron de "(bloqueado)" a Integration normal — ver la nota de la tabla de clasificación. Ningún contrato quedó reubicado silenciosamente: cada edición se muestra como diff, se aprobó explícitamente y se escribió al GDD dueño. Residual fuera de alcance de OQ-P6: AC-P06b (OQ-P2, forma exacta de la señal/hook de Héroes) y AC-P16 (Forja de Legado sigue sin GDD) permanecen bloqueados por razones propias, no por esta pasada. + +**OQ-P5 — Patrón de implementación: ¿Autoload, nodo de escena, o event bus? → ADR (parcialmente resuelto).** +**Ya decidido por ADR-0002 (Accepted 2026-08-21):** la publicación del régimen de tiempo (`game_time_paused`, `time_scale`, `game_tick`) NO la hace Permadeath — la hace el servicio Core **TimeControl** vía la API `request_regime`/`release_regime`; Permadeath es solo un *solicitante* de `PAUSED`. Esto retira de este OQ la sub-decisión de "cómo publicar el flag" (señal-push híbrida vs polling) — vive ahora en TimeControl. **Queda abierto para el ADR propio de Permadeath:** Autoload vs nodo de escena vs event bus para la orquestación de la cola FIFO; el `reset_queue()` explícito y quién lo dispara (clase de softlock, ver Gap 2026-08-15 abajo); la forma exacta de `animation_done`/`force_relic_reveal_final()` (OQ-P2); y el clock exento del beat (que ADR-0002 confirma como "no sondear TimeControl"). **Acción**: crear ese ADR con `/architecture-decision` antes de implementar. + +**Sub-decisiones para el ADR (recomendaciones del godot-specialist, re-review 2026-08-14):** +- **Publicación de `game_time_paused`: preferir señal-push híbrida sobre polling puro.** Exponer una señal `game_time_paused_changed(bool)` para las transiciones de flanco **más** un getter booleano para consultas de estado de consumidores que se suscriben tarde. El polling puro por frame es frágil: un consumidor cuyo `_process` no corre (p. ej. congelado por una pausa real) no puede pollear en absoluto, y un guard `if game_time_paused: return` olvidado falla en silencio. La señal fuerza la notificación; el getter cubre el estado inicial. +- **Spike de motor — elevado a gate de Approved, no solo del ADR (2026-08-15).** Los docs curados del proyecto (`docs/engine-reference/godot/`) no tienen entradas sobre `SceneTree.paused` / `process_mode` / delta unscaled / `Engine.time_scale` para 4.4–4.6 — las afirmaciones de motor de este GDD están *sin verificar por docs*, no *confirmadas seguras*. Sin embargo, Regla 4/Edge Cases y varios AC de Integration (AC-P07, P07b, P07c, P12–P14) ya asertan este comportamiento como hecho consumado, en secciones que se tratan como mecánica decidida — así que el spike no puede quedar diferido hasta la implementación o el cierre del ADR: **debe correrse y confirmar sus resultados antes de que este GDD pueda pasar a Approved.** Alcance del spike: (a) `PROCESS_MODE_ALWAYS` tickea con delta unscaled mientras el árbol NO está pausado; (b) comportamiento de `SceneTree.paused` con el menú de pausa global; (c) **interacción con `Engine.time_scale`** — confirmar que la exención de pausa (`process_mode`) y la escala de tiempo (`time_scale`, que un futuro sistema de hit-stop/slow-mo podría usar) son mecanismos independientes, de modo que el reloj del beat (`elapsed_s`) no se escale silenciosamente aunque su nodo sea pause-exempt. + +**Sub-cuestión de diseño (SÍ pertenece a este GDD, no solo al ADR): ¿el estado de runtime de Permadeath se reinicia entre eras?** La cola FIFO y la entrada `HOLDING` actual son estado de sesión; un Autoload persiste por defecto entre recargas de escena salvo que se limpie explícitamente. Las Edge Cases cubren el crash/reload (donde el héroe ya se sella vía Regla 8b), pero **no** el caso normal de transición de era: al terminar una era, la cola debe quedar vacía (todas las muertes de la era ya resueltas y drenadas) y Permadeath debe empezar la siguiente era sin arrastrar estado. **Mecanismo (recomendación godot-specialist, re-review 2026-08-14 #2):** Godot 4.6 **no** provee ningún hook de motor que dispare "al cambiar de era" que un Autoload pueda escuchar pasivamente (solo el ruidoso `tree_changed`, no un "scene changed" limpio). Por tanto el ADR debe especificar un `Permadeath.reset_queue()` (o equivalente) **explícito**, llamado por quien orquesta la Transición de Era — nada lo hace implícitamente. Conviene además re-leer/re-validar `N_max` de la nueva era en ese mismo punto (sobre su señal era-`LOADED`), coherente con la cardinalidad por-era de la Regla 8. **Acción**: fijar como regla explícita — "al iniciar una era, la cola de Permadeath está vacía; no persiste entradas entre eras; el reset lo dispara la Transición de Era vía `reset_queue()`" — al confirmar la Transición de Era (sin GDD aún), y validarlo en el spike de motor 4.6 ya planificado. + +**Gap encontrado en esta revisión (2026-08-15): `reset_queue()` atado solo a la Transición de Era completa es insuficiente — clase de softlock.** Un reload a mitad de era (reintento tras derrota, carga manual, o el path de recuperación por crash de la Regla 8b) no necesariamente dispara la señal era-`LOADED` de una Transición de Era completa. Si `reset_queue()` solo escucha ese hook, un reload de ese tipo puede dejar `HOLDING`/`QUEUED` fantasma en la cola — y como Input permanece suprimido mientras existe una entrada activa (Regla 4/PU-2), el resultado es supresión de input permanente sin ningún beat real corriendo detrás para liberarla. El invariante correcto no es "la cola se vacía entre eras" sino **"la cola de Permadeath está vacía en todo punto donde el juego devuelve control al jugador desde un estado cargado"** — cubre tanto la Transición de Era completa como cualquier reload a mitad de era. **Acción ampliada**: `reset_queue()` (o una verificación equivalente de cola-vacía) debe invocarse como parte de **toda** secuencia de carga que resume el juego, no solo del hook de Transición de Era — antes de que Input pueda salir de cualquier estado de supresión heredado de una sesión anterior. No es una decisión de diseño nueva, es cerrar un caso que el alcance original de esta regla no cubría; se fija en el mismo ADR/spike ya planificado (OQ-P5). diff --git a/design/gdd/reliquias-bendiciones.md b/design/gdd/reliquias-bendiciones.md new file mode 100644 index 0000000000..51aa58281f --- /dev/null +++ b/design/gdd/reliquias-bendiciones.md @@ -0,0 +1,578 @@ +# Reliquias/Bendiciones + +> **Status**: In Design +> **Author**: usuario + agentes +> **Last Updated**: 2026-08-18 (design-review 2026-08-18: NEEDS REVISION → 7 bloqueantes aplicados; pendiente re-review en sesión limpia) +> **Implements Pillar**: El Pasado es Poder (Pilar 2 — sistema eje); Preparación Ritual, Clímax Explosivo (Pilar 3) + +## Overview + +Reliquias/Bendiciones es el sistema que cierra la cadena central del juego (Permadeath → Forja de Legado → **Reliquias/Bendiciones** → Panteón): toma las Reliquias que Forja de Legado ya forjó a partir de muertes de héroes de eras anteriores y las convierte en **bendiciones invocables durante el clímax de combate** contra el kaiju. A nivel de datos, es la capa de traducción y balance de la cadena de legado: define qué efecto mecánico corresponde a cada combinación de `relic_category` (`offensive`/`defensive`/`rally`) × `tier` (Delgada/Sólida/Legendaria) sin romper la regla de daño plano de Combate/Daño, gestiona el pool acumulado de reliquias del que se sortea (estilo Hades — el jugador elige 1 de varias opciones ofrecidas), y limita cuántas veces se puede invocar una bendición por era (recurso finito, deliberadamente no farmeable — coherente con el core loop del concepto). A nivel de jugador, es el momento exacto en que el legado deja de ser un objeto pasivo exhibido en el panteón y se vuelve **poder activo en la mano del jugador**: la reliquia con el nombre de un héroe caído aparece como una opción táctica real en medio del enfrentamiento contra el próximo kaiju, y elegirla es reafirmar, en el fragor del combate, que esa muerte importó. Sin este sistema, Forja de Legado produciría reliquias sin propósito jugable y Permadeath un veredicto sin consecuencia mecánica visible — Reliquias/Bendiciones es lo que hace que el **Pilar 2** ("El Pasado es Poder") sea literalmente cierto en el momento que más importa: el clímax. + +## Player Fantasy + +*(Nota: el GDD se autoró en modo lean sin `creative-director`; el encuadre fue revisado por `creative-director` en el design-review del 2026-08-18, que confirmó que la fantasía es la razón de ser del sistema y exigió cerrar el gap de entrega —identidad glanceable + reconciliación de la oferta sin-pausa con el telegrafío del kaiju— aplicado en esta revisión. `art-director` aún pendiente para producción de VFX/íconos.)* + +**El jugador es el comandante que empuña, en pleno fragor de combate, el arma forjada con la muerte de alguien que perdió.** El momento de invocar una bendición no es elegir un ítem de una lista genérica — es un instante de reconocimiento: entre las opciones ofrecidas hay reliquias con nombre propio, cada una la huella mecánica de una tragedia específica y vivida por el propio jugador en una era anterior. La tensión de "¿cuál elijo?" (estilo Hades — tres opciones, decisión rápida bajo presión de clímax) se funde con una pregunta que Hades nunca hace: *"¿a cuál de mis muertos invoco ahora?"*. Es la diferencia entre un power-up y una invocación. + +Debajo de ese instante directo hay una capa que el jugador siente sin verla: la escasez. **Precisión de qué es escaso (reconciliación cross-review 2026-08-18):** el panteón de reliquias *crece* con el tiempo — es una colección, no un consumible; las reliquias no se gastan al usarse (vuelven al pool). Lo verdaderamente escaso, lo que no se puede "farmear" ni acumular, son las **`invocation_charges` por era**: pocas, se resetean por era, no se acumulan entre eras. Esa cota por-era es lo que vuelve significativa cada elección — no cuántas reliquias tengas, sino cuántas veces puedes invocar *esta* era. Si el jugador pudiera invocar sin límite, el panteón se volvería un inventario más; la escasez de cargas es lo que preserva el peso del Pilar 1 dentro del Pilar 2 — cada invocación gasta algo real. *(Nota: la calidad de las reliquias acumuladas sí está gateada por `relic_quality` — una muerte descuidada solo da Delgada; ver la interacción con el anti-turtle de Furia en encuentro-con-kaiju.md OQ-15.)* + +**Test de diseño (este sistema debe hacerlo cumplir):** si el efecto de una bendición pudiera pertenecer, sin cambiar una palabra, a un drop de loot genérico sin nombre — fallamos. Toda bendición debe sentirse indisociable del héroe y la circunstancia que la forjó (categoría + tier + epitafio), nunca intercambiable con "una poción +10% daño" cualquiera. Referencia directa: Darkest Dungeon logra este peso con las cicatrices de sus héroes; Hades lo logra con la excitación del draft — este sistema necesita ambas cosas a la vez, no una eligiendo sobre la otra. + +## Detailed Design + +### Core Rules + +1. **Fuente única del pool (solo lectura sobre Forja de Legado).** El pool de invocación se construye leyendo el registro de Reliquias que **posee Forja de Legado** (la `RelicRecord`). Reliquias/Bendiciones **nunca** forja, modifica ni borra una reliquia — solo consume `relic_id`, `relic_category`, `tier`, `source_hero_name`, `epitaph` y `origin_era_id` de cada registro. El `origin_era_id` es lo que permite a F-RB5 excluir del pool las reliquias forjadas en la era en curso (Regla 10, mecanismo de exclusión intra-era). Esto **cierra Forja OQ-FL2 / desbloquea AC-FL19** (Forja definió el contrato provisional; este sistema lo confirma). + +2. **Mapeo `relic_category → tipo de efecto` (fijo, temático).** Cada categoría tiene una identidad mecánica invariable y legible: + - **`offensive`** → efecto **ofensivo**: bono de daño plano aditivo a un héroe/banda y/o una **acción ofensiva invocable** (p. ej. un golpe único de área). + - **`defensive`** → efecto de **supervivencia**: bono plano de vida máxima temporal, mitigación temporal, o un aura defensiva anclada (conceptualmente pariente de `guard_radius`). + - **`rally`** → efecto de **grupo/tropas**: refuerzo instantáneo o buff plano aplicado a toda la banda del jugador. + La categoría es fija por reliquia (transportada de Forja/Héroes, nunca reasignada). El *qué* hace una bendición lo determina su categoría, no su tier. + +3. **Mapeo `tier → magnitud` (escalonado, mismo efecto).** El tier (`Delgada`/`Sólida`/`Legendaria`, derivado por Forja vía FL1) escala **solo la magnitud** del efecto de su categoría: Delgada = base, Sólida = base × `solid_factor`, Legendaria = base × `legendary_factor` (factores = knobs, afinados en Formulas). El tier **nunca** cambia *qué* hace la bendición, solo *cuánto* — preserva la legibilidad ("una muerte mejor gastada da una bendición más fuerte, no una distinta"). Reliquias/Bendiciones **consume** el tier ya resuelto por Forja; **nunca recalcula** `relic_quality` ni redefine los cortes de tier (los posee Héroes). + +4. **Todo efecto respeta la Regla 5 de Combate/Daño (daño plano, sin multiplicadores de tipo).** Ningún bono de daño es un multiplicador porcentual sobre la fórmula de daño. Los bonos ofensivos son **aditivos y planos** (`+N` al daño por golpe, aplicado como modificador de instancia temporal); las **acciones invocables** (golpe de área, refuerzo) usan el camino `apply_damage` / spawn **ya existente** de los sistemas dueños, con montos fijos — nunca introducen una fórmula de daño nueva. Un golpe de área se implementa como **múltiples llamadas `apply_damage` de un solo objetivo** sobre los enemigos dentro de un radio (Combate es single-target, sin hook de cleave — se respeta iterando, no ampliando la fórmula). Esta regla es la que mantiene el Pilar 3 legible. + +5. **Cargas de invocación: recurso finito por era (anti-farmeo).** El jugador dispone de `invocation_charges` por era (knob, MVP pocas — ~2–3; posible config por `EraDefinition`). Invocar gasta **exactamente una** carga. Sin cargas restantes, no se puede invocar. Las cargas **se resetean al valor de la era, no se acumulan entre eras** — es lo que hace literal el core loop del concepto ("recurso limitado, no se puede abusar") y preserva el peso del Pilar 1 dentro del Pilar 2. + +6. **Invocación = sorteo 3-choose-1 desde el pool acumulado (estilo Hades).** Al **abrir** una invocación (con ≥1 carga disponible), el sistema **sortea `draw_size` reliquias** (knob, MVP 3) del pool acumulado y las ofrece; el jugador **elige 1** y en ese momento **se gasta exactamente una carga** (commit-on-choice, Regla 7b); se aplica el efecto de la reliquia elegida. Las **no elegidas vuelven al pool** (no se consumen — una reliquia no se "gasta", lo que se gasta es la carga). El sorteo usa un RNG **inyectable/semilla-controlable** (misma disciplina que Encuentro con Kaiju para tests deterministas). + +7. **Ventana de invocación: durante el `CLIMAX` (contra el kaiju).** La invocación está disponible mientras el Temporizador esté en fase `CLIMAX` (el enfrentamiento activo) — es donde el concepto ubica las bendiciones (Core Mechanic #3) y donde el Pilar 3 alcanza su pico. *(Si conviene permitir bendiciones defensivas/rally de preparación en `PREPARATION`, se decide como afinación — ver Open Questions; el MVP arranca solo-`CLIMAX` por simplicidad.)* + +7b. **La oferta corre en cámara lenta, no en pausa (`offering_time_scale`).** Mientras el estado es `OFFERING` (oferta abierta, esperando la elección del jugador), la simulación se **ralentiza** a `offering_time_scale` (knob, MVP 0.2×) en lugar de detenerse. Consecuencias, todas deliberadas: **(a)** el jugador conserva el control de sus unidades durante la oferta (puede seguir dando órdenes — la pelea no se congela); **(b)** el kaiju y su telegrafío también se ralentizan al mismo factor, de modo que **leer una reliquia nunca puede provocar una emboscada** (el telegrafío no puede completarse "a espaldas" del jugador mientras lee) — esto reconcilia el momento de reconocimiento con la regla de "sin pausa" (Pilar 3): el tiempo no se detiene, solo se dilata; **(c)** el jugador **sigue siendo vulnerable** — puede recibir daño en cámara lenta si ya estaba en la trayectoria de un golpe, así que la oferta no es un refugio de invulnerabilidad. **Timing de la carga (commit-on-choice, no commit-on-open).** La carga se gasta en el **momento de elegir** (`OFFERING`→`RESOLVING`), no al abrir la oferta — coherente con AC-RB40 ("la carga se gasta al elegir"). Abrir una oferta y cancelarla **no cuesta carga**. Para evitar que la cámara lenta se explote como stall gratuito e ilimitado, dos frenos: **(1) soft-timeout** (`offering_soft_timeout_s`, knob, MVP 8.0s de tiempo real): al expirar sin elección, la oferta se auto-cancela (vuelve a `READY`) sin gastar carga; **(2) cooldown post-cancelación** (`offering_cooldown_s`, knob, MVP 3.0s de tiempo real a velocidad normal): tras cualquier cancelación (timeout o manual), `can_invoke=false` durante el cooldown, así el jugador no puede re-abrir la cámara lenta de inmediato. Juntos acotan la cámara lenta a ráfagas cortas y espaciadas, sin penalizar la indecisión con una carga perdida. La propiedad del mecanismo de escala de tiempo global es un contrato a coordinar (ver OQ-RB9). + +8. **Reliquias/Bendiciones no posee combate, stats base ni la fórmula de daño.** Es **productor de efectos, consumidor de APIs existentes**: delega todo daño en Combate/Daño (`apply_damage`), los refuerzos/buffs de tropa en Sistema de Tropas, y los buffs de héroe en Sistema de Héroes. Los bonos se aplican como **modificadores de instancia temporales** (duración = knob), **nunca** mutando el `CombatProfile`/`HeroDefinition`/`TroopDefinition` persistente — la fuente de verdad de stats no se ensucia. + +9. **Sistema inerte sin legado (onboarding natural).** Si el pool está vacío (no hay reliquias forjadas todavía — el caso de la primera era antes de la primera muerte), **no se ofrece invocación** y el sistema es inerte. Esto realiza el gate de onboarding del concepto ("las bendiciones aleatorias no se exponen hasta la 2ª partida") **sin un candado artificial** — simplemente no hay nada que invocar hasta que la cadena de legado produce su primera reliquia. + +10. **El pool se construye SOLO de reliquias de eras anteriores; nunca de muertes de la era en curso (decisión fijada, cierra OQ-RB5).** El pool de invocación de una era se puebla exclusivamente con reliquias forjadas en eras **previas** (el panteón heredado). Una muerte de héroe **durante** el CLIMAX en curso forja su reliquia (vía Permadeath→Forja) pero esa reliquia **no** entra al pool invocable de este mismo combate — recién queda disponible a partir de la era siguiente. Esto preserva el peso trágico (Pilar 1/2): una muerte no se convierte en un ítem de vending-machine que el jugador draftea 90 segundos después en la misma pelea, sino en poder heredado por la próxima civilización. **Para el MVP de una sola era**, esto implica que el pool debe **pre-sembrarse** desde un panteón de prueba (roster de muertes de una "era 0" ficticia) para poder demostrar el hook de bendiciones — de lo contrario la primera era arranca con pool vacío y el sistema es inerte toda la partida (coherente con la Regla 9 y con la curva de onboarding del concepto: las bendiciones no se exponen en la 1ª partida). *(La forma exacta del panteón de prueba pre-sembrado — cuántas reliquias, qué mezcla de categoría/tier — se fija al poblar el save inicial; ver OQ-RB5.)* + +### States and Transitions + +*(Ciclo por-invocación — no un estado global del sistema. Un estado global ligero por-era, `CHARGES_AVAILABLE` vs `DEPLETED`, envuelve este ciclo.)* + +| Estado | Descripción | Transición desde | Transición hacia | +|---|---|---|---| +| `READY` | Hay ≥1 carga, el pool no está vacío, fase = `CLIMAX`, y no en cooldown post-cancelación | Inicio de `CLIMAX`; fin de una invocación previa con cargas restantes; fin del cooldown post-cancelación | `OFFERING` (jugador abre la oferta — aún NO gasta carga) | +| `OFFERING` | Oferta abierta: `draw_size` reliquias sorteadas y ofrecidas; **simulación en cámara lenta (`offering_time_scale`)**; esperando elección; carga **aún NO gastada** | `READY` | `RESOLVING` (jugador elige → gasta 1 carga); `READY` (cancelación manual o soft-timeout → **sin gasto de carga**, entra a cooldown) | +| `RESOLVING` | Reliquia elegida; **carga gastada al elegir**; efecto aplicándose vía la API del sistema dueño; simulación vuelve a velocidad normal | `OFFERING` | `ACTIVE` (efecto con duración) o directamente `SPENT` (efecto instantáneo) | +| `ACTIVE` | Efecto temporal en curso (duración corriendo) | `RESOLVING` | `SPENT` (duración expira) | +| `SPENT` | Efecto terminado; carga consumida; no-elegidas devueltas al pool | `RESOLVING` / `ACTIVE` | `READY` (quedan cargas) / `DEPLETED` (0 cargas) | +| `DEPLETED` | 0 cargas restantes esta era; no se puede invocar | `SPENT` | — (hasta reset de la próxima era) | + +### Interactions with Other Systems + +| Sistema | Dirección | Interfaz | +|---|---|---| +| **Forja de Legado** | Forja → este (**solo lectura**) | Lee el registro de `RelicRecord` (`relic_id`, `relic_category`, `tier`, `source_hero_name`, `epitaph`, `forge_index`, **`origin_era_id`**) para construir el pool — `origin_era_id` es el campo que implementa la exclusión intra-era de la Regla 10 (F-RB5). **Cierra Forja OQ-FL2 / desbloquea AC-FL19** — confirma y extiende el contrato provisional `get_relic_combat_inputs() → {relic_category, tier}` para incluir también los campos de identidad (nombre/epitafio) que la UI necesita mostrar. Nunca escribe | +| **Combate/Daño** | este → Combate | Los efectos ofensivos con daño llaman `apply_damage` (camino single-target existente; área = iterar sobre objetivos en radio). Bonos de daño = modificadores planos aditivos temporales. **Llena la interfaz downstream placeholder de Combate** ("las bendiciones se invocan/resuelven en combate… su interfaz exacta se define al diseñarse"). ✅ Combate Approved | +| **Sistema de Tropas** | este → Tropas | Efectos `rally` (refuerzo instantáneo) vía las APIs de Tropas, surtiendo `min(N, band_headroom)` (Tropas Fórmula E; `max_band_size` es autorado por Datos de Era). Este sistema lee `band_headroom` read-only, no posee el tope. ✅ Designed (F2-B5 resuelto) | +| **Sistema de Héroes** | este → Héroes | Efectos ofensivos/defensivos que buffean a un héroe aplicados como modificadores de instancia temporales (nunca al `HeroDefinition` persistente). ✅ Approved | +| **UI/HUD** | este → UI/HUD | Expone `active_blessings: list` y la oferta de sorteo (3-choose-1) para renderizar durante `CLIMAX`. **Cierra UI/HUD AC-U30 / OQ-U4** (owner era `game-designer` "cuando este GDD exista"). ✅ Approved | +| **Temporizador de Preparación/Ritual** | Temporizador → este (solo lectura) | Lee la **fase** (`CLIMAX`) para habilitar la ventana de invocación (Regla 7). Consume, nunca posee. ✅ Approved | +| **Datos de Era/Civilización** | Datos → este (solo lectura) | `invocation_charges` (y posiblemente `draw_size`) pueden autorarse por era vía `EraDefinition`; además, lee el `era_id` de la era ACTIVE (`active_era_id`) para la exclusión intra-era del pool (Regla 10 / F-RB5). ✅ Designed | +| **Guardado/Persistencia** | este ↔ Guardado | El pool (panteón) lo persiste Forja; las **cargas gastadas y el estado de invocación intra-era** deben restaurarse en una recarga a mitad de era. Contrato a coordinar — ver Open Questions. ✅ Designed | +| **Salón Conmemorativo/Panteón** (#14) | — (sin dependencia directa) | Ambos leen el mismo registro de Forja; el Panteón exhibe, este invoca. Sin GDD aún | + +> **Nota de fuente de verdad:** Reliquias/Bendiciones **no duplica** ningún dato de reliquia — el dueño canónico es Forja de Legado (`RelicRecord`). Este sistema mantiene únicamente estado **efímero de invocación** (cargas restantes, efectos activos, oferta actual), no datos de legado. + +## Formulas + +*(Especialistas consultados, MANDATORY en lean: `economy-designer` (primary — números de balance/economía) + `systems-designer` (mecánica/determinismo), en paralelo. Convergieron; los valores abajo son su propuesta conjunta, aprobada por el usuario.)* + +Reliquias/Bendiciones no calcula daño (eso es Combate/Daño vía `apply_damage`) — sus fórmulas gobiernan **magnitud del efecto por tier**, **apilado de modificadores temporales**, **el sorteo**, **la expiración por tick** y **la composición del pool**. Todos los efectos son bonos planos aditivos (Regla 4 / Combate Regla 5); ningún multiplicador porcentual. + +### Efectos MVP por categoría (un efecto limpio por categoría) + +| Categoría | Efecto MVP | Timing | Base (Delgada) | Sólida (×1.5) | Legendaria (×2.25) | Anclaje | +|---|---|---|---|---|---|---| +| `offensive` | `+N` daño plano por golpe a un héroe | Temporizado | +8 | +12 | +18 | Base = daño de un `melee_infantry` (8); +72% al daño de Vanguardia (25) en Legendaria, dentro de una ventana limitada | +| `defensive` | `+N` vida máxima temporal a un héroe | Temporizado | +30 | +45 | +68 | Sólida (45) = exactamente un golpe de kaiju (45): "compra un golpe que te mataría" | +| `rally` | `N` refuerzos instantáneos a la banda | Instantáneo | 2 | 3 | 5 | Conteos redondean **hacia arriba** (4.5→5) — nunca escatimar el tier alto en un entero visible | + +*Diferido a post-MVP (fuera de alcance MVP, no borrado): golpe de área ofensivo (AoE), mitigación plana defensiva (chocaría con la regla "sin armadura MVP" de Combate), aura defensiva anclada, buff de banda `rally` temporizado.* + +**Anclaje cross-categoría (las tres categorías deben ser competitivas entre sí, no solo dentro de su categoría).** El riesgo de diseño es que una categoría domine matemáticamente: el bono ofensivo (+18 Legendaria) es **valor incondicional** (siempre daña más), mientras el defensivo (+68 vida máx) es **valor contingente** (solo vale si el héroe estuvo a punto de morir en la ventana), y el `rally` aún no está anclado (OQ-RB8). Bajo economía de valor esperado, el valor incondicional domina al contingente salvo que la probabilidad de muerte sea alta — lo que haría de ofensivo la elección "correcta" la mayoría de las corridas y colapsaría la tensión "¿cuál invoco?". **Reglas de anclaje explícitas para el MVP:** + +- **El anclaje defensivo se define por golpes-de-kaiju-comprados, no por vida cruda.** Sólida defensiva (+45) = exactamente 1 golpe de kaiju (45); Legendaria defensiva (+68) = 1.51 golpes. Esto es deliberado: la Legendaria defensiva debe comprar **estrictamente más de un golpe** (>1.0) para justificar el tier, pero **no dos** (evita el "héroe casi inmortal" que el knob `defensive_hp_base` flagea en su rango). El valor se lee contra el daño del kaiju, la única amenaza cuyo timing el jugador no controla. +- **El techo del ofensivo se ancla a la legibilidad del Pilar 3, no al DPS.** Legendaria ofensiva (+18) mantiene al daño de Vanguardia buffeado (25+18=43) **por debajo** del umbral de one-shot sobre esbirros de referencia y **por debajo** del daño del kaiju (45), preservando que el clímax siga siendo legible (ningún golpe de héroe trivializa un tramo). El knob `offensive_damage_base` ya codifica este techo (>12 rompe legibilidad). +- **La comparación se valida por telemetría, no se asume resuelta.** Con `effect_duration_s=20` cubriendo ~un ciclo de telegrafío del kaiju, el ofensivo casi siempre ve uso; el defensivo solo si el héroe recibe daño en la ventana. Si la telemetría de vertical slice muestra que ofensivo se elige >X% frente a defensivo/rally independientemente del contexto, es señal de dominancia a corregir (subir el piso de valor incondicional del defensivo — p. ej. la mitigación plana diferida — o bajar el ofensivo). **Flageado en OQ-RB8.** +- **`rally` no tiene anclaje numérico confirmado en el MVP** (su valor depende del tamaño típico de banda en CLIMAX, un dato que aún no existe): sus números (2/3/5 refuerzos) son provisionales y **no cross-validables** contra ofensivo/defensivo hasta vertical slice (OQ-RB8). Se documenta como pendiente, no como resuelto. + +### F-RB1 — Stat efectivo con modificadores temporales de instancia + +La fórmula del stat efectivo de una unidad se define como: + +`effective_stat(u, s, t) = base_stat(u_type, s) + Σ magnitude(m) para cada m en active_modifiers(u, s, t)` + +**Variables:** +| Variable | Símbolo | Tipo | Rango | Descripción | +|----------|---------|------|-------|-------------| +| Instancia de unidad | `u` | id de instancia | — | El héroe/tropa concreto, **no** su tipo | +| Stat | `s` | enum | `{attack_damage, max_health}` (MVP) | Qué stat se modifica | +| Stat base | `base_stat(u_type, s)` | int | ≥1 | Leído **solo-lectura** del `CombatProfile`/`HeroDefinition` del tipo — nunca mutado | +| Modificadores activos | `active_modifiers(u, s, t)` | set | tamaño ≥0 | Todas las instancias de bendición activas sobre `u` para `s` en el tick `t` | +| Magnitud por modificador | `magnitude(m)` | int | ≥1 | Bono plano por modificador (F-RB2) | +| **Stat efectivo** | `effective_stat` | int | `[base_stat, ∞)` | Lo que Combate lee al resolver un golpe | + +**Rango de salida:** `[base_stat, ∞)` — los bonos solo suman (las bendiciones nunca debuffean). Sin cap en el MVP: el limitador anti-abuso ya es la economía de cargas (2–3/era), no un tope de stack. + +**Apilado (regla explícita):** **aditivo entre reliquias distintas; una sola instancia activa por `relic_id` por unidad (refresh-on-repick).** Bendiciones de reliquias *distintas* activas sobre el mismo héroe suman. Pero re-elegir una reliquia que **ya tiene una instancia activa sobre esa misma unidad** NO apila una segunda instancia: **refresca la duración** de la instancia existente a un `expiry_tick` completo nuevo (la magnitud no cambia — el tier es el mismo). Esto elimina el burst degenerado de una sola reliquia: 3 cargas sobre la misma Legendaria ofensiva ya **no** producen 25→79; producen 25→43 (una instancia, refrescada dos veces). Cada `relic_id` activo sobre una unidad tiene a lo sumo un modificador; reliquias distintas mantienen modificadores independientes, cada uno con su propio `expiry_tick`. + +**Cota de poder simultáneo (worst-case computable — cierra el gap "sin fórmula" de la nota de knobs).** El bono máximo a un stat de una unidad está acotado por: +`max_stat_bonus(s) = Σ (magnitude de las hasta D reliquias distintas del stat s de mayor magnitud que el jugador puede tener activas simultáneamente)`, donde `D = min(invocation_charges, nº de reliquias distintas del stat s en el pool, nº de reliquias del stat s que caben en effect_duration_s antes de que la primera expire)`. Con MVP `invocation_charges=3` y `max_legendary_per_draw=1`, el peor caso realista para `attack_damage` requiere **3 reliquias Legendaria ofensivas DISTINTAS** en el pool, las tres sorteadas y elegidas sobre el mismo héroe dentro de la ventana → 25 + 3×18 = 79. El refresh-on-repick lo hace **mucho menos probable** (ya no basta que reaparezca una sola Legendaria), pero el stacking entre reliquias distintas sigue siendo aditivo por diseño (combinar legados de héroes distintos es temáticamente válido). Este residual queda **flageado para validación por telemetría en vertical slice** (ver OQ-RB8) — no se clampa en runtime (coherente con la filosofía loud-fail del proyecto), pero ahora es una cantidad *computable y acotada*, no unbounded. + +**Contrato de Combate (resuelto — ADR-0001, Accepted 2026-08-21):** Combate expone el read-hook `effective_attack_damage(instancia)` (y la API de escritura `add_modifier`/`remove_by_source`/`sweep_expired`) sobre una capa `CombatState` **por instancia** que capa este stack de modificadores sobre el `CombatProfile` del tipo **en el momento de resolver el golpe**. Este sistema deposita modificadores planos aditivos vía esa API y **nunca** escribe en `CombatProfile`. Ver `docs/architecture/adr-0001-combat-stat-modifier-layer.md`. Cierra OQ-RB1. + +**Ejemplo:** Vanguardia (`attack_damage`=25) con una bendición ofensiva Sólida (+12) y otra ofensiva Delgada (+8) → `effective_stat = 25 + 12 + 8 = 45`. + +### F-RB2 — Magnitud por tier (escalonada) + +`magnitude(base_value, tier) = round_half_up(base_value × tier_factor(tier))` +donde `tier_factor(Delgada)=1.0`, `tier_factor(Sólida)=solid_factor`, `tier_factor(Legendaria)=legendary_factor`. + +**Variables:** +| Variable | Símbolo | Tipo | Rango | Descripción | +|----------|---------|------|-------|-------------| +| Valor base | `base_value` | int | ≥1 | Magnitud Delgada del efecto (por categoría, tabla arriba) | +| Factor Sólida | `solid_factor` | float | >1.0 (MVP 1.5) | Escala del tier medio | +| Factor Legendaria | `legendary_factor` | float | `> solid_factor` (MVP 2.25) | Escala del tier alto | +| **Magnitud** | `magnitude` | int | ≥`base_value` | Bono plano aplicado, redondeado a entero | + +**Rango de salida:** `[base_value, ∞)`, estrictamente creciente entre tiers por el invariante de abajo. El tier **nunca** cambia *qué* hace la bendición, solo *cuánto*. Consume el tier de Forja (FL1); nunca recalcula `relic_quality`. + +**Invariante loud-fail (carga, no clamp — precedente del proyecto):** para cada `base_value` en uso, verificar `magnitude(_,Sólida) > magnitude(_,Delgada)` y `magnitude(_,Legendaria) > magnitude(_,Sólida)`. El redondeo puede colapsar silenciosamente una brecha de tier débil (p. ej. `base_value=1, solid_factor=1.3 → round(1.3)=1` = Delgada) — eso debe fallar ruidosamente al cargar, no shippear como "Sólida se siente igual que Delgada". + +**Ejemplo:** `base_value=8` (offensive), factors 1.5/2.25 → Delgada 8, Sólida `round(12.0)=12`, Legendaria `round(18.0)=18`. Estrictamente creciente ✓. + +### F-RB3 — Sorteo 3-choose-1 (muestreo sin reemplazo, RNG semillado) + +`k_effective = min(draw_size, P)` +`offer = fisher_yates_partial(pool, k_effective, rng_seeded)` + +**Variables:** +| Variable | Símbolo | Tipo | Rango | Descripción | +|----------|---------|------|-------|-------------| +| Pool | `pool` | lista ordenada de `relic_id` | tamaño `P ≥ 0` | Pool activo (F-RB5), ordenado por `forge_index` ascendente (determinista) | +| Tamaño de sorteo | `draw_size` | int | ≥1 (MVP 3) | Cuántas reliquias se ofrecen | +| RNG | `rng_seeded` | instancia RNG | inyectable/semilla | Misma disciplina que Encuentro con Kaiju (AC-K71) | +| **Oferta** | `offer` | lista de `relic_id` | tamaño `k_effective` | Lo presentado al jugador | + +**Rango de salida:** `|offer| = min(P, draw_size)`. **Si `P < draw_size`** (caso MVP real, justo tras la primera muerte): se ofrecen las que haya (p. ej. `P=2, draw_size=3` → 2-choose-1), nunca se rellena ni se lanza error. **Si `P=0`:** no se alcanza en la práctica (Regla 9 / estado `READY` exige pool no vacío); loud-fail defensivo si se invoca con `P=0` (indica bug de máquina de estados aguas arriba). + +**Techo de Legendaria por sorteo (Rule B):** ningún `offer` contiene más de `max_legendary_per_draw` reliquias Legendaria (MVP **1**). Garantiza que una Legendaria ofrecida nunca compite contra otra Legendaria — mantiene su singularidad, independiente de cuán Legendaria-pesado sea el pool. El algoritmo exacto (sub-pool ponderado vs. reroll de exceso) es de implementación. + +**Ejemplo:** `pool=[R1..R5]` (orden forge_index), `draw_size=3`, con ≤1 Legendaria → `offer` = 3 reliquias, bit-idéntico para el mismo seed+pool. + +### F-RB4 — Expiración por tick (disciplina de física, coordinada con Combate Regla 1) + +`duration_ticks = ceil(duration_s × physics_ticks_per_second)` +`expiry_tick = applied_tick + duration_ticks` +`is_active(t) = applied_tick ≤ t < expiry_tick` (intervalo semiabierto) + +**Variables:** +| Variable | Símbolo | Tipo | Rango | Descripción | +|----------|---------|------|-------|-------------| +| Duración | `duration_s` | float | >0 (MVP 20.0) | Duración de efectos temporizados (knob) | +| Ticks/seg | `physics_ticks_per_second` | int | setting (MVP 60) | Paso fijo de física — **nunca** `_process(delta)`/wall-clock | +| Tick de aplicación | `applied_tick` | int | ≥0 | Contador de tick al aplicar (RESOLVING→ACTIVE) | +| Tick de expiración | `expiry_tick` | int | `> applied_tick` | Tick en que se remueve el modificador | + +**Rango de salida:** `expiry_tick > applied_tick` siempre (por `duration_s>0`). La expiración se chequea **al inicio de cada tick, antes** de la fase-1 de daño de Combate — un modificador con `expiry_tick == t` ya no existe para ningún `apply_damage` del tick `t` (sin ambigüedad de orden). Efectos **instantáneos** (refuerzo): aplicados una vez en RESOLVING, sin bookkeeping de ticks, van directo a `SPENT`. + +**Expiración defensiva segura (edge case preempted):** el bono de vida máxima se aplica `+N` a `max_health` **y** `current_health` a la vez (heal+shield). Al expirar, `max_health` baja `−N` y `current_health = min(current_health, nuevo_max_health)` (clamp hacia abajo, **nunca** vía `apply_damage`). La expiración solo puede *topar* la vida actual, nunca empujarla por debajo de lo que un golpe real ya podría — **expirar nunca mata a un héroe.** + +**Ejemplo:** `duration_s=20`, 60 ticks/s → `duration_ticks=1200`. `applied_tick=1000` → activo en `[1000, 2200)` = exactamente 20.0s de tiempo de juego, independiente del framerate. + +### F-RB5 — Composición del pool activo (Rule A: exclusión intra-era + tope + retiro FIFO) + +`eligible_relics_count = count(reliquias con origin_era_id ≠ active_era_id)` +`active_pool_size = min(eligible_relics_count, pool_cap)` + +**Variables:** +| Variable | Símbolo | Tipo | Rango | Descripción | +|----------|---------|------|-------|-------------| +| Era activa | `active_era_id` | id | — | `era_id` de la era en estado ACTIVE (leído de Datos de Era/Civilización) | +| Reliquias elegibles | `eligible_relics_count` | int | ≥0 | Reliquias forjadas en eras **distintas** de la activa (`origin_era_id ≠ active_era_id`) — implementa Regla 10 | +| Tope de pool | `pool_cap` | int | ≥1 (MVP 16, rango [10,24]) | Máximo de reliquias invocables a la vez | +| **Tamaño de pool activo** | `active_pool_size` | int | `[0, pool_cap]` | Cuántas reliquias están en el pool de sorteo | + +**Exclusión intra-era (Regla 10 — mecanismo).** El pool **excluye toda reliquia cuyo `origin_era_id` sea la era ACTIVE**: una muerte de héroe durante el CLIMAX en curso forja su reliquia (Permadeath→Forja) pero esa reliquia **no** entra al pool invocable de este mismo combate — recién es elegible a partir de la era siguiente. Esto es lo que hace implementable la Regla 10 (antes el contrato de lectura omitía `origin_era_id` y esta fórmula no filtraba por era). *(En el MVP de una sola era con panteón pre-sembrado desde una "era 0" ficticia, todas las pre-sembradas tienen `origin_era_id ≠ active_era_id` y pasan el filtro — el hook de bendiciones se demuestra sin exponer una muerte intra-era como ítem invocable.)* **Warning S1 (feedback de UX, no mecanismo):** la reliquia recién forjada simplemente **no aparece** en ninguna oferta esta era (no está en el pool elegible), lo cual resuelve el mecanismo — pero el jugador podría no entender *por qué* la reliquia de su héroe caído no es invocable. Un cue diegético opcional ("consagrada — disponible la próxima era") es una mejora de UX del overlay de invocación, **diferida a `/ux-design`** (mismo destino que OQ-U8); no bloquea el MVP. + +**Rango de salida:** `[0, pool_cap]`. Cuando `eligible_relics_count > pool_cap`, la reliquia elegible de **menor `forge_index`** **se retira del pool invocable** → pasa a **solo-exhibición en Panteón** (sigue mostrada y honrada, nunca borrada — la `RelicRecord` no se toca; solo cambia el estado efímero "está en mi pool de sorteo" que este sistema posee). El retiro FIFO opera sobre las reliquias **elegibles**, ordenadas por `forge_index`, determinista. Da al Panteón (#14) un rol narrativo: retirada ≠ olvidada, retirada = consagrada. + +**Por qué dos reglas (A + B):** un pool más grande **no** da más ni mayores bendiciones por invocación (la magnitud F-RB2 nunca lee el tamaño del pool). El riesgo real es doble: (1) crecimiento ilimitado diluye la promesa narrativa de "reliquia con nombre propio" → **Rule A** (tope); (2) drift de composición (sorteos cada vez más Legendaria-pesados) → **Rule B** (techo por sorteo, F-RB3). Juntas acotan tamaño Y magnitud por invocación, independiente del nº de eras. + +### Invariantes loud-fail (carga) + +- `draw_size ≥ 1`; `invocation_charges ≥ 1`; `duration_s > 0` para todo efecto temporizado. +- `solid_factor > 1.0` **y** `legendary_factor > solid_factor`. +- `magnitude` estrictamente creciente entre los 3 tiers para cada `base_value` (atrapa el colapso por redondeo). +- `base_value ≥ 1` para todo bono plano; `pool_cap ≥ 1`; `0 ≤ max_legendary_per_draw ≤ draw_size`. + +Cada violación es config rota, no un extremo válido — falla ruidosa, nunca clamp silencioso (precedente de Héroes/Combate). + +## Edge Cases + +- **Si el pool está vacío al entrar a `CLIMAX`** (primera era sin muertes previas): no se ofrece invocación y el sistema permanece inerte (`READY` nunca se alcanza — exige pool no vacío, Regla 9). No es bug: es el gate de onboarding natural. El HUD no muestra affordance de invocación. + +- **Si `P < draw_size`** (pool más chico que el sorteo — p. ej. 2 reliquias forjadas, `draw_size=3`): se ofrecen las `P` que haya (2-choose-1), nunca se rellena con duplicados ni se lanza error (F-RB3). Estado esperado de "corrida temprana", no UX degradada. + +- **Si el jugador re-elige la misma `relic_id` sobre la misma unidad en una invocación posterior** (posible y frecuente con pool chico, donde las mismas reliquias aparecen en cada sorteo): si esa reliquia **ya tiene una instancia activa sobre esa unidad**, no se apila una segunda — se **refresca la duración** de la existente a una ventana completa nueva (F-RB1, AC-RB31). El bono de esa reliquia a ese stat nunca excede una sola magnitud. Esto es deliberado: acota el burst degenerado de una sola reliquia (era el vector unbounded principal). *(Re-elegirla sobre una unidad **distinta** sí crea un modificador propio en esa otra unidad — el límite es por-`relic_id`-por-unidad, no global.)* + +- **Si el jugador intenta invocar sin cargas restantes** (`DEPLETED`): la acción se rechaza; el HUD indica no-disponibilidad sin texto editorializante (understatement del proyecto). No consume nada. + +- **Si el jugador intenta invocar fuera de `CLIMAX`** (durante `PREPARATION`/`IMMINENT` en el MVP): la invocación no está disponible (Regla 7). El affordance solo aparece en `CLIMAX`. + +- **Si un efecto temporizado de vida máxima expira** (`defensive`): `max_health` baja `−N` y `current_health = min(current_health, nuevo_max_health)` — clamp hacia abajo, **nunca** vía `apply_damage` (F-RB4). La expiración solo puede topar la vida actual hacia un valor ya alcanzable por combate normal: **expirar nunca mata a un héroe**, ni dispara `DYING`/permadeath. + +- **Si el héroe con un buff temporizado activo muere antes de que el efecto expire**: el modificador se descarta con la instancia del héroe al entrar a `DYING` (el buff no sobrevive a su portador ni se transfiere). La muerte se clasifica normalmente por Combate (`D`/`P`) sin que el buff altere la clasificación — un bono de daño no cuenta como "propósito servido". + +- **Si un refuerzo `rally` instantáneo se invoca y no hay sitio/límite de banda** (tope de unidades de la era alcanzado): se refuerza hasta el límite disponible; si el límite ya está saturado, el refuerzo es un no-op parcial/nulo pero **la carga igual se gasta** (la carga se gasta al elegir, no al surtir efecto — coherente con Regla 5). **Mitigación del trap:** la oferta expone `band_headroom` por cada opción `rally` (RU-3b), de modo que UI/HUD puede señalar (no-textual) que un `rally` rendiría 0 o parcial **antes** de que el jugador elija — el jugador no "gasta a su amigo caído para nada" sin previo aviso. Caso a coordinar con el tope de banda de Sistema de Tropas/Datos de Era. + +- **Si el jugador recibe daño mientras la oferta está abierta (`OFFERING`)**: sí puede recibirlo — la simulación corre en cámara lenta, no en pausa (`offering_time_scale`, Regla 7b). Un golpe que ya estaba en trayectoria conecta en cámara lenta; la oferta **no** es un refugio de invulnerabilidad. Pero como el telegrafío del kaiju también se ralentiza al mismo factor, **ningún telegrafío puede completarse "a espaldas" del jugador** mientras lee — no hay emboscada causada por abrir la oferta. Si el héroe elegido como objetivo de un buff muere durante la oferta, ver la regla de descarte de buff por muerte (el modificador nunca se aplica a una unidad muerta; si era el único objetivo válido, la elección puede reofertar o resolver como no-op según la categoría — coordinar en implementación, OQ-RB7). + +- **Si el juego se recarga a mitad de era** (autosave/crash): el pool (panteón) lo restaura Forja/Guardado; las **cargas gastadas y los efectos temporizados activos** deben restaurarse al estado guardado. *(La forma exacta de persistir el estado efímero de invocación — cargas restantes, modificadores activos con su tick de expiración restante — es un contrato a coordinar con Guardado; ver Open Questions. Decisión de diseño ya fijada: las cargas gastadas NO se devuelven por recargar — anti-abuso.)* + +- **Si `total_relics_forged` cruza `pool_cap`** durante la era (una muerte intra-era forja la reliquia nº 17 con `pool_cap=16`): la reliquia de menor `forge_index` en el pool activo se retira a solo-exhibición en Panteón (F-RB5, FIFO determinista) en el momento del depósito. La `RelicRecord` nunca se borra. Si esto ocurre mientras esa reliquia estaba siendo ofrecida en un sorteo abierto (`OFFERING`), la oferta en curso se respeta hasta resolverse (no se retira una reliquia bajo los pies del jugador a mitad de elección); el retiro aplica al siguiente sorteo. + +- **Si dos efectos modifican el mismo stat del mismo héroe con timers desfasados**: ambos suman mientras ambos estén activos (F-RB1, sin cap); al expirar el primero, su `−N` se remueve y el segundo sigue hasta su propio `expiry_tick`. Sin interacción entre timers — cada modificador es independiente. + +- **Si el efecto ofensivo diferido (AoE) se referencia por error en el MVP**: no existe punto de entrada — está fuera de alcance (tabla de efectos). Un `relic_category=offensive` produce únicamente el buff de daño plano temporizado en el MVP. + +## Dependencies + +**Dependencias hacia arriba (upstream) — lo que este sistema necesita:** + +| Sistema | Tipo | Interfaz | Estado | +|---|---|---|---| +| **Forja de Legado** | Dura (**productor crítico**) | Lee el registro de `RelicRecord` (solo lectura) para construir el pool: `relic_id`, `relic_category`, `tier`, `source_hero_name`, `epitaph`, `forge_index`, **`origin_era_id`** (este último implementa la exclusión intra-era de la Regla 10 / F-RB5). **Cierra Forja OQ-FL2 / desbloquea AC-FL19** — este GDD confirma y extiende su contrato provisional `get_relic_combat_inputs()` para incluir los campos de identidad | ✅ Designed (`forja-de-legado.md`) — pendiente su propio design-review | +| **Combate/Daño** | Dura (bidireccional) | Los efectos con daño llaman `apply_damage` (camino single-target). **Requiere un read-hook nuevo** `effective_attack_damage(instancia)` que capa los modificadores temporales sobre el `CombatProfile` del tipo — no existe en `combate-dano.md` hoy | ⚠️ **Contrato provisional** — Combate Approved, pero el read-hook es un añadido pequeño a su contrato (ver OQ-RB1). Llena la interfaz downstream placeholder que Combate ya reservó para este sistema | +| **Temporizador de Preparación/Ritual** | Dura (**solo lectura**) | Lee la fase (`CLIMAX`) para habilitar la ventana de invocación (Regla 7). Consume, nunca posee | ✅ Approved (`temporizador-de-preparacion-ritual.md`) | +| **Sistema de Tropas** | Dura | Aplica efectos `rally` (refuerzo instantáneo) vía las APIs de Tropas, surtiendo `min(N, band_headroom)`; lee `band_headroom` read-only (Tropas Fórmula E, tope `max_band_size` de Datos de Era). Nunca muta el `TroopDefinition` persistente | ✅ Designed (`sistema-de-tropas.md`) — F2-B5 resuelto 2026-08-21 | +| **Sistema de Héroes** | Dura | Aplica buffs ofensivos/defensivos como modificadores de instancia sobre el héroe; nunca muta el `HeroDefinition` persistente | ✅ Approved (`sistema-de-heroes.md`) | +| **Datos de Era/Civilización** | Blanda (datos, solo lectura) | `invocation_charges` (y posiblemente `draw_size`, `pool_cap`) pueden autorarse por era vía `EraDefinition`; además lee `active_era_id` (el `era_id` ACTIVE) para la exclusión intra-era del pool (Regla 10 / F-RB5) | ✅ Designed (`datos-de-era-civilizacion.md`) | +| **Guardado/Persistencia** | Dura (bidireccional) | El pool lo persiste Forja; el **estado efímero de invocación** (cargas gastadas, modificadores activos con tick de expiración restante) debe restaurarse en recarga a mitad de era | ✅ Designed — forma exacta del contrato a coordinar (ver OQ-RB2) | +| **Input** | Dura (bidireccional) | Este sistema solicita a Input entrar al contexto `BLESSING_SELECT`; Input emite `blessing_choice_confirmed(index)` / `blessing_selection_cancelled` que este sistema consume para el draft 3-choose-1 (RU-2 delega la entrada que dispara la invocación a Input/Control). El draft corre en `CLIMAX` en cámara lenta | ✅ Designed (`input.md`) — bidireccionalidad registrada 2026-08-19; contrato de señal en `input.md` Sección Interactions | + +**Dependientes hacia abajo (downstream) — dependen de este:** + +| Sistema | Tipo | Interfaz | +|---|---|---| +| **UI/HUD** | Dura | Consume `active_blessings: list` y la oferta de sorteo (3-choose-1) para renderizar durante `CLIMAX`. **Cierra UI/HUD AC-U30 / OQ-U4** (owner era `game-designer` "cuando este GDD exista"). ✅ Approved | +| **Salón Conmemorativo/Panteón** (#14) | Blanda | Exhibe las reliquias, incluidas las **retiradas del pool activo** por el tope FIFO (F-RB5) — "retirada = consagrada". Ambos leen el registro de Forja; sin dependencia directa entre sí. *Sin GDD aún — contrato provisional* | + +**Nota de consistencia bidireccional:** +- **Forja de Legado** lista a Reliquias/Bendiciones como consumidor crítico downstream (OQ-FL2, AC-FL19 bloqueado) → ✅ coincide con este upstream; este GDD lo **desbloquea**. +- **UI/HUD** tiene `AC-U30` bloqueado + `OQ-U4` esperando este GDD → ✅ coincide con este downstream; **desbloqueado**. +- **Combate/Daño** lista a Reliquias/Bendiciones como dependiente downstream con interfaz placeholder ("su interfaz exacta se define al diseñarse") → ✅ este GDD la llena; y el read-hook `effective_attack_damage` **ya está absorbido por Combate vía ADR-0001 (Accepted 2026-08-21)** — la capa `CombatState` por instancia es dueña de la aplicación y la expiración. OQ-RB1 cerrada. +- **Temporizador** ya expone la fase `CLIMAX` que este sistema consume → sin edición requerida. +- `systems-index.md` lista la fila de Reliquias/Bendiciones con "Depends On: Forja de Legado, Combate/Daño" → debería añadir **Temporizador de Preparación/Ritual**, **Sistema de Tropas**, **Sistema de Héroes**, **Guardado/Persistencia** y **Datos de Era/Civilización** (⚠️ ver Fase 5 / OQ-RB3). + +**Nota provisional:** Salón Conmemorativo/Panteón (#14) aún no tiene GDD — su contrato es propuesto por este documento. Todos los demás sistemas listados ya tienen GDD (Designed o Approved); sus contratos con este sistema están confirmados salvo el read-hook de Combate (OQ-RB1). + +## Tuning Knobs + +| Knob | Símbolo | Tipo | Default MVP | Rango seguro | Qué se rompe en extremos | +|---|---|---|---|---|---| +| Factor Sólida | `solid_factor` | float | 1.5 | 1.3–1.75 | <1.3: Sólida ≈ Delgada, el tier deja de leerse como progresión. >1.75: se acerca demasiado a Legendaria, debilita la escalera de 3 tiers | +| Factor Legendaria | `legendary_factor` | float | 2.25 | 1.8–2.75 | <1.8: Legendaria no se siente ganada (el band `[0.70,1.00]` de `relic_quality` es difícil de lograr, el payoff debe verse). >2.75: una sola Legendaria trivializa un tramo del CLIMAX y erosiona la decisión de qué invocar | +| Cargas por era | `invocation_charges` | int | 3 | 2–4 | <2: una sola decisión en un CLIMAX largo, degrada a un pre-buff único. >4: erosiona la escasez y sube la carga cognitiva en tiempo real (no hay pausa) | +| Tamaño de sorteo | `draw_size` | int | 3 | 2–4 (≥1 duro) | 1: no hay elección (deja de ser draft). >4: demasiadas opciones que leer bajo presión de clímax sin pausa | +| Duración de efectos temporizados | `effect_duration_s` | float | 20.0 | 15.0–30.0 | <15: el buff expira a mitad del telegrafío del kaiju (ciclo ~14s+), rompe la fantasía "invoqué esto para sobrevivir el próximo golpe". >30: casi-permanente, las cargas dejan de ser el limitador principal | +| Escala de tiempo de la oferta | `offering_time_scale` | float | 0.2 | 0.1–0.5 (015: la cámara lenta se estira demasiado, diluye la tensión de tiempo real | +| Cooldown post-cancelación | `offering_cooldown_s` | float | 3.0 | 1.5–6.0 (tiempo real) | <1.5: la cámara lenta se puede re-abrir casi sin fricción (farmeable). >6: castiga en exceso una cancelación honesta, el jugador evita abrir por miedo al lockout | +| Tope de pool activo | `pool_cap` | int | 16 | 10–24 | <10: la variedad de sorteo colapsa (mismas reliquias repetidas, mata la frescura Hades). >24: reliquias individuales dejan de ser reconocibles/especiales; el rol narrativo "reliquia consagrada" del Panteón nunca se usa | +| Techo de Legendaria por sorteo | `max_legendary_per_draw` | int | 1 | 1–2 (0 prohibido) | 0: ninguna Legendaria aparece jamás — contradice todo el payoff de ganar una. >2 (=`draw_size`): revierte a sorteo uniforme, reabre el drift de composición (riesgo ALTO flageado) | +| Base ofensiva (daño plano) | `offensive_damage_base` | int | 8 | 4–12 | <4: bono ofensivo imperceptible incluso en Legendaria. >12: Legendaria (×2.25=27) rivaliza con el daño base de Vanguardia (25), one-shots que rompen la legibilidad del Pilar 3 | +| Base defensiva (vida máx) | `defensive_hp_base` | int | 30 | 20–40 | <20: no compra ni un golpe de kaiju (45), el efecto no se siente. >40: Legendaria (×2.25=90) = dos golpes de kaiju, un héroe casi inmortal por 20s | +| Base rally (refuerzos) | `rally_reinforce_base` | int | 2 | 1–3 | <1: no refuerza nada. >3: Legendaria (×2.25→7) inunda la banda, diluye la escasez de tropas del Pilar 3 | + +**Interacción entre knobs:** +- `solid_factor` y `legendary_factor` están acoplados por el invariante de monotonía (F-RB2): `1.0 < solid_factor < legendary_factor`, verificado con loud-fail contra cada `base_value`. Bajar un `*_base` demasiado puede colapsar la brecha de tier por redondeo — el invariante lo atrapa al cargar. +- `invocation_charges` × `draw_size` × `effect_duration_s` gobiernan juntos cuánto poder simultáneo puede tener el jugador. Subir los tres a la vez multiplica el poder de forma no obvia — coordinar cambios. +- `pool_cap` y `max_legendary_per_draw` son las dos palancas del balance de pool a largo plazo (F-RB5/F-RB3, riesgo ALTO): tamaño y magnitud por invocación respectivamente. Son independientes del nº de eras por diseño. + +**Knobs que NO existen (decisiones conscientes):** +- **No hay knob de cortes de tier**: los boundaries `[0.10,0.35)/[0.35,0.70)/[0.70,1.00]` los posee Héroes (`relic_quality`); este sistema los consume vía Forja FL1, no los duplica (evita segunda fuente de verdad). +- **No hay knob de multiplicador porcentual de daño**: prohibido por la Regla 4 / Combate Regla 5 (daño plano). Todo bono es aditivo. +- **No hay knob de pausa**: la invocación es tiempo real por decisión de diseño (Pilar 3), no configurable. +- **No hay knob de duración por tier**: el tier escala solo magnitud (Regla 3), no duración — meterlo compondría poder de forma runaway en Legendaria. + +## Visual/Audio Requirements + +*Referencia rectora: Art Bible Principio A ("El Legado es Luz") — la reliquia dorada es el caso central del principio. **Reliquias/Bendiciones no diseña el ícono de la reliquia** (lo posee Forja de Legado, FV-1) ni la escenificación de la muerte (Héroes V-6/Permadeath). Esta sección fija el contrato visual/sonoro del **momento de invocación en combate** — lo único que este sistema posee. Modo lean: `art-director` no spawneado (categoría no-visual-required estricta, mismo criterio que Forja); flag de re-visita manual antes de producción.* + +**RV-1 — El dorado del legado entra al combate solo en la invocación.** El mundo de combate es apagado (Principio A: dorado reservado al legado). El único dorado pleno durante `CLIMAX` aparece en el **momento de invocación**: la oferta de reliquias y el destello del efecto al aplicarse. Fuera de ese instante, el combate no tiene dorado (coherente con Combate CV-1/Principio A: "nunca dorado en combate rutinario"). Esto hace que invocar se *sienta* como traer el pasado sagrado al presente. + +**RV-2 — La oferta 3-choose-1 usa el ícono de reliquia de Forja + identidad glanceable del héroe.** Cada opción del sorteo se presenta con el **ícono dorado que Forja ya determinó** (`relic_category` × `tier`, FV-1) más su identidad. El **canal de reconocimiento primario es glanceable, no textual**: retrato o silueta del héroe caído + su nombre, legible de un vistazo bajo presión de clímax (aun en cámara lenta, RU-3) — porque el momento "¿a cuál de mis muertos invoco?" debe resolverse por *quién* fue, no por leer prosa. El `epitaph` es canal **secundario** (procedencia que refuerza, no de la que depende el reconocimiento). El grado de acabado dorado escala con `tier` (Delgada = plomo apenas insinuado; Legendaria = dorado pleno) — el jugador *ve* qué tier elige sin leer un número. Reliquias/Bendiciones no crea íconos nuevos; compone la oferta con los de Forja + el retrato del héroe (owned por Héroes/Forja — ver nota de individuación abajo). + +> **Nota de individuación (limitación conocida, honesta con el design-test de la Sección Player Fantasy).** El efecto mecánico y el `epitaph` de una reliquia derivan solo de `relic_category` × `tier` — **9 combinaciones**, con el `epitaph` tomado de una tabla-plantilla de 9 entradas (owned por Forja, AC-FL09). Esto significa que dos reliquias ofensivas-Sólida de héroes distintos son idénticas en efecto y en plantilla de epitafio, diferenciándose solo por **quién** las forjó. El canal glanceable (retrato + nombre) es lo que las individualiza en el momento de la oferta; sin él, el sistema fallaría su propio design-test ("nunca intercambiable con un drop genérico"). **Individuación mecánica/textual más profunda** (epitafios que citen la circunstancia específica de la muerte, no solo la plantilla categoría×tier) **requiere coordinación con Forja** — está fuera del alcance de este GDD porque el `epitaph` lo posee Forja; se flagea como recomendación cross-GDD (ver OQ-RB10). + +**RV-3 — El efecto de bendición se telegrafía por categoría, no por número.** Al aplicarse, cada categoría tiene un lenguaje visual distinto y legible a escala RTS (Principio: distinción por forma/canal, no solo color): `offensive` (realce dorado sobre el arma/silueta del héroe buffeado), `defensive` (aura/envoltura dorada breve sobre el héroe), `rally` (destello dorado sobre la banda + aparición de refuerzos). La **magnitud (tier)** se lee por intensidad/duración del realce, redundante con el ícono elegido — nunca solo por color (daltonismo, coherente con UI/HUD HU-10 ≥3 canales). + +**RV-4 — Sin dorado persistente que ensucie la legibilidad del clímax.** Los realces de bendición son **temporales** (ligados a `effect_duration_s`) y no deben competir con el telegrafío del kaiju (Encuentro KV-1) ni con el readout de peligro del héroe (UI/HUD). Un buff activo se indica de forma sostenida-pero-sutil (no un pulso dorado constante que distraiga); el pico dorado es el instante de aplicación, luego decae a un indicador tenue. Límite con UI/HUD: *cuándo/cómo* se ancla el indicador sostenido lo decide UI/HUD (Sección UI). + +**Audio *(provisional — sin dirección de audio aún, mismo estatus que Combate/Héroes)*:** + +**RA-1 — La invocación tiene un verbo sonoro propio, coral/sagrado.** A diferencia de Combate (cuyo audio de muerte de héroe es handoff a Permadeath, understatement), la invocación **sí** es un momento sonoro afirmativo — el pasado respondiendo al llamado. Registro coral/orquestal breve (coherente con el tono trágico-sagrado del concepto), categóricamente distinto del lecho de combate. Se duckea el lecho de combate en el instante de aplicación, luego retorna. + +**RA-2 — El tier modula la magnitud sonora, no el motivo.** Una Legendaria suena más plena/resonante que una Delgada, pero es reconociblemente el mismo verbo de "invocación" — el jugador oye *qué tan potente* fue la muerte que forjó esa reliquia, sin un motivo distinto por tier (coherente con RV-3 y con el outcome-blind-pero-identity-varied de Héroes A-3). + +📌 **Asset Spec** — Con Visual/Audio definido, tras aprobar el art bible se puede correr `/asset-spec system:reliquias-bendiciones` para producir specs de VFX por-asset (realce de invocación por categoría, destello de aplicación, indicador de buff sostenido). + +## UI Requirements + +*Reliquias/Bendiciones no diseña pantallas — expone datos y eventos que UI/HUD renderiza con el lenguaje diegético del art bible (Sección 7). Esta sección fija el **contrato de qué fluye a UI/HUD**, desbloqueando su `AC-U30`/`OQ-U4`. El *cómo* se ve lo decide UI/HUD.* + +**RU-1 — Exposición de cargas restantes.** Expone `invocation_charges_remaining` (int) y su tope de la era, para que UI/HUD muestre cuántas invocaciones quedan durante `CLIMAX`. Read-only para UI/HUD. + +**RU-2 — Affordance de invocación gateada por estado.** Expone si la invocación está **disponible ahora** (`can_invoke`: fase = `CLIMAX` **∧** cargas > 0 **∧** pool no vacío **∧** no en cooldown post-cancelación, Regla 7b). UI/HUD muestra/oculta el affordance en función de este booleano; la entrada que dispara la invocación la posee Input/Control (este sistema solo expone disponibilidad y consume el evento de "invocar"). + +**RU-2b — Onboarding de la primera invocación (`first_invocation_available`).** Para que un jugador de primera partida **descubra** que la invocación existe (el affordance aparece por primera vez a mitad de un combate contra el kaiju), este sistema expone un evento único `first_invocation_available` la primera vez que `can_invoke` pasa a `true` en la campaña del jugador (distinto del `can_invoke` de estado estable). UI/HUD lo consume para un beat de onboarding (resalte/one-time cue) — el *cómo* lo decide UI/HUD, pero el dato existe para construirlo. Se dispara a lo sumo una vez por save. + +**RU-3 — La oferta 3-choose-1 (`blessing_offer`).** Al abrir una invocación, expone la oferta como una lista de vistas de reliquia (tamaño `k_effective`, 1–`draw_size`): por cada una, `relic_id`, ícono (de Forja, `relic_category`×`tier`), **identidad glanceable** (`source_hero_name` + retrato/silueta del héroe, ver RV-2 — el canal de reconocimiento primario, legible de un vistazo), `relic_category`, `tier`, y `epitaph` (canal **secundario** — texto de procedencia que se lee si el jugador tiene tiempo, no la señal de la que depende el reconocimiento). UI/HUD la renderiza como draft de opciones; el jugador elige una; UI/HUD devuelve el `relic_id` elegido. **En cámara lenta, no en pausa** (Regla 7b, `offering_time_scale`) — la simulación se ralentiza mientras la oferta está abierta, de modo que el momento de reconocimiento ("¿a cuál de mis muertos invoco?") es legible sin congelar el juego ni exponer al jugador a una emboscada. El reconocimiento debe poder resolverse por la **identidad glanceable** (quién + tier por brillo del ícono), no por leer prosa de epitafio bajo presión. + +**RU-3b — Headroom de banda visible en ofertas `rally` (evita el trap de whiff).** Para cada opción `rally` de la oferta, **relaya** `band_headroom` (int ≥0): cuántos refuerzos caben realmente dada la ocupación actual de banda vs. el tope de la era. **Fuente (F2-B5 resuelto 2026-08-21):** `band_headroom` lo **posee Sistema de Tropas** (Fórmula E, `= max(0, max_band_size − alive_troop_count)`); `max_band_size` es autorado por Datos de Era. Este sistema solo lo lee y lo pasa por opción; un `rally` de tier `N` surtirá `min(N, band_headroom)` tropas. UI/HUD lo usa para señalar (no-textual, coherente con RU-5) cuándo un `rally` rendiría 0 o parcial **antes** de que el jugador gaste la carga — cerrando el "gasté a mi amigo caído para nada" sin previo aviso (ver Edge Case de `rally` en tope de banda). + +**RU-4 — Bendiciones activas (`active_blessings: list`) — desbloquea UI/HUD AC-U30.** Expone la lista de efectos temporizados activos: por cada uno, categoría, tier, y **tiempo/tick de expiración restante** (para un indicador de duración). Esto es exactamente la `active_blessings: list` que UI/HUD `AC-U30`/`OQ-U4` asumía como mock — este contrato la vuelve real. UI/HUD la renderiza sin mutarla (read-only). + +**RU-5 — Sin editorializar (understatement).** Coherente con Combate CU-4 / Héroes U-5: este sistema no expone texto de "¡Bendición invocada!" ni juicios. Expone estado estructurado (cargas, oferta, activos) e identidad de reliquia (nombre/epitafio, que son *dato de procedencia*, no editorial). La lectura del momento es visual/sonora (RV-1..3/RA-1), no un banner de sistema. + +**RU-6 — Feedback de rechazo no-silencioso, no-texto.** Si el jugador intenta invocar sin cargas o fuera de `CLIMAX` (`can_invoke=false`), expone un evento de rechazo que UI/HUD refleja con un cue no-textual (coherente con el patrón de rechazo de Sacrificio de UI/HUD) — nunca un silencio ambiguo ni un texto editorializante. + +📌 **UX Flag — Reliquias/Bendiciones**: Este sistema alimenta la UI de invocación en combate (affordance de cargas, oferta 3-choose-1, indicador de bendiciones activas) — la interacción más densa del HUD de `CLIMAX`, en tiempo real sin pausa. En Pre-Producción, correr `/ux-design` para la pantalla/overlay de invocación **antes** de escribir epics; las stories que referencien esa UI deben citar el spec de UX, no este GDD. Coordinar con el HUD de `CLIMAX` de UI/HUD (ya Approved) para no duplicar ni colisionar con el telegrafío del kaiju y el readout de peligro del héroe. + +## Acceptance Criteria + +*Convención: formato Dado/Cuando/Entonces + etiqueta de tipo de evidencia (`[Logic/unit]`, `[Integration]`). Los AC marcados **(bloqueado)** dependen de un contrato no confirmado hoy — hoy verificables solo con mock en el límite de este sistema. Forja de Legado, Temporizador, Sistema de Tropas, Sistema de Héroes, Datos de Era/Civilización y Guardado/Persistencia **sí** tienen GDD — los AC contra esos contratos NO están bloqueados, solo pendientes de implementación (mismo criterio que `forja-de-legado.md` aplicó). Solo bloquean: el read-hook `effective_attack_damage` de Combate/Daño (OQ-RB1, no está en `combate-dano.md` hoy), el contrato de persistencia de estado efímero de invocación con Guardado (OQ-RB2, a coordinar), y el Salón Conmemorativo/Panteón (#14, sin GDD).* + +### A. Fuente del pool — solo lectura sobre Forja de Legado (Regla 1) + +**AC-RB01 — El pool se construye leyendo `RelicRecord` de Forja; nunca escribe** **[Integration]** +Dado el registro de `RelicRecord` que posee Forja de Legado, cuando Reliquias/Bendiciones construye su pool de invocación, entonces lee únicamente `relic_id`, `relic_category`, `tier`, `source_hero_name`, `epitaph`, `forge_index`, `origin_era_id` de cada registro (`origin_era_id` implementa la exclusión intra-era de la Regla 10, F-RB5) — no invoca ningún método de forja, edición o borrado sobre Forja. + +**AC-RB02 — Reliquias/Bendiciones nunca forja, modifica ni borra una reliquia** **[Logic/unit]** +Dado la superficie pública de Reliquias/Bendiciones hacia Forja de Legado, cuando se audita qué métodos invoca, entonces solo llama hooks de lectura (p. ej. `list_relics()`/`get_relic_combat_inputs()`) — no expone ni invoca ningún método capaz de crear, editar o eliminar una `RelicRecord`. *(Se complementa con un check estático de CI — ver nota de testabilidad al final.)* + +### B. `relic_category` → tipo de efecto, fijo (Regla 2) + +**AC-RB03 — `offensive` siempre produce el efecto ofensivo** **[Logic/unit]** +Dado una reliquia con `relic_category = offensive` de cualquier tier, cuando se resuelve su efecto, entonces el efecto aplicado es el bono de daño plano por golpe (tabla MVP) — nunca un efecto defensivo o `rally`. + +**AC-RB04 — `defensive` siempre produce el efecto de supervivencia** **[Logic/unit]** +Dado una reliquia con `relic_category = defensive` de cualquier tier, cuando se resuelve su efecto, entonces el efecto aplicado es el bono temporal de vida máxima — nunca ofensivo o `rally`. + +**AC-RB05 — `rally` siempre produce el efecto de grupo/tropas** **[Logic/unit]** +Dado una reliquia con `relic_category = rally` de cualquier tier, cuando se resuelve su efecto, entonces el efecto aplicado son refuerzos instantáneos a la banda — nunca ofensivo o defensivo. + +**AC-RB06 — La categoría nunca se reasigna; el tier no cambia el tipo de efecto** **[Logic/unit]** +Dado una misma reliquia observada hipotéticamente en Delgada, Sólida y Legendaria, cuando se resuelve el tipo de efecto en cada caso, entonces el tipo permanece idéntico a través de los tres tiers — solo cambia la magnitud (F-RB2), nunca el *qué*. + +### C. `tier` → magnitud, escalonado (Regla 3, Fórmula F-RB2) + +**AC-RB07 — Delgada = magnitud base (`tier_factor = 1.0`)** **[Logic/unit]** +Dado un `base_value` de cualquier categoría, cuando se evalúa F-RB2 con `tier = Delgada`, entonces `magnitude = base_value` sin escalar. + +**AC-RB08 — Sólida = `base_value × solid_factor` (1.5), redondeado** **[Logic/unit]** +Dado `base_value = 8` (offensive), cuando se evalúa F-RB2 con `tier = Sólida`, entonces `magnitude = round_half_up(12.0) = 12`. + +**AC-RB09 — Legendaria = `base_value × legendary_factor` (2.25), redondeado** **[Logic/unit]** +Dado `base_value = 8` (offensive), cuando se evalúa F-RB2 con `tier = Legendaria`, entonces `magnitude = round_half_up(18.0) = 18`. + +**AC-RB10 — `round_half_up` aplica sobre los tres `base_value` de la tabla MVP** **[Logic/unit]** +Dado `base_value = 30` (defensive) y `base_value = 2` (rally), cuando se evalúan Sólida/Legendaria, entonces los resultados son `{45, 68}` y `{3, 5}` respectivamente (`round_half_up(4.5) = 5`, nunca hacia abajo). + +**AC-RB11 — Invariante loud-fail: magnitud estrictamente creciente entre los 3 tiers, por `base_value`** **[Logic/unit]** +Dado un `base_value` donde el redondeo colapsaría la brecha entre tiers (p. ej. `base_value = 1`, `solid_factor = 1.3` → `round(1.3) = 1` = igual a Delgada), cuando se carga la configuración, entonces falla ruidosamente en build de desarrollo — nunca shipea un tier que "se siente igual" al anterior. + +**AC-RB12 — Reliquias/Bendiciones consume el tier de Forja (FL1); nunca recalcula `relic_quality` ni redefine cortes de tier** **[Logic/unit]** +Dado un `tier` recibido de una `RelicRecord`, cuando este sistema resuelve la magnitud del efecto, entonces usa ese `tier` tal cual — no invoca ninguna lógica de mapeo `relic_quality → tier` propia (esa lógica es de Forja FL1 exclusivamente). + +### D. Daño plano, sin multiplicadores porcentuales (Regla 4 / Combate Regla 5) + +**AC-RB13 — El bono ofensivo es aditivo plano, nunca un multiplicador porcentual** **[Logic/unit]** +Dado un bono ofensivo de cualquier tier, cuando se aplica sobre `effective_stat` (F-RB1, la capa de modificadores propia de este sistema), entonces se suma como cantidad fija `+N` — nunca se expresa ni aplica como porcentaje del stat base. *(Verificable hoy contra F-RB1 sin depender del read-hook de Combate: la integración con `effective_attack_damage` la cubre AC-RB32, que sí está bloqueado por OQ-RB1.)* + +**AC-RB14 — Los efectos MVP usan APIs de daño/spawn ya existentes, nunca una ruta nueva** **[Integration]** +Dado el efecto `rally` (refuerzos instantáneos), cuando se aplica, entonces invoca la API de spawn/reclutamiento ya existente de Sistema de Tropas — Reliquias/Bendiciones no introduce un camino de spawn ni de daño propio, ni siquiera para el golpe de área diferido a post-MVP (fuera de alcance hoy — ningún punto de entrada existe para él). + +### E. Cargas de invocación — recurso finito por era, anti-farmeo (Regla 5) + +**AC-RB15 — Elegir una reliquia gasta exactamente 1 carga; abrir la oferta no gasta** **[Logic/unit]** +Dado `invocation_charges_remaining = N > 0`, cuando el jugador **abre** una oferta (`READY`→`OFFERING`) la carga **no** cambia (`= N`); cuando luego **elige** una reliquia (`OFFERING`→`RESOLVING`), entonces `invocation_charges_remaining = N - 1` — exactamente una carga, nunca 0 ni 2, y solo en el momento de elegir (commit-on-choice, Regla 7b). + +**AC-RB15b — Cancelar una oferta (manual o soft-timeout) no gasta carga y entra a cooldown** **[Logic/unit]** +Dado una oferta abierta (`OFFERING`) con `invocation_charges_remaining = N`, cuando el jugador cancela manualmente o transcurre `offering_soft_timeout_s` sin elección, entonces el estado vuelve a `READY` con `invocation_charges_remaining = N` (sin gasto), y `can_invoke = false` durante `offering_cooldown_s` de tiempo real — impidiendo re-abrir la cámara lenta de inmediato. + +**AC-RB16 — Sin cargas restantes → no se puede invocar** **[Logic/unit]** +Dado `invocation_charges_remaining = 0` (`DEPLETED`), cuando el jugador intenta invocar, entonces la acción se rechaza sin gastar nada y sin sortear ninguna oferta. + +**AC-RB17 — Las cargas se resetean al valor de la era; nunca se acumulan entre eras** **[Integration]** +Dado un valor `invocation_charges` autorado en `EraDefinition` para la era N+1, cuando la era N+1 se activa, entonces `invocation_charges_remaining` se fija a ese valor de la nueva era — independientemente de cuántas cargas quedaran sin gastar (o se gastaran) al final de la era N. + +### F. Sorteo 3-choose-1 (Regla 6, Fórmula F-RB3) + +**AC-RB18 — El sorteo ofrece `min(draw_size, P)` reliquias con RNG inyectable/semillado** **[Logic/unit]** +Dado un pool de `P ≥ draw_size` reliquias y un `rng_seeded` con semilla fija, cuando se invoca F-RB3, entonces `|offer| = draw_size` y el resultado es reproducible bit-idéntico para el mismo seed y el mismo pool. + +**AC-RB19 — `P < draw_size` → se ofrecen las `P` que haya, nunca se rellena ni se lanza error** **[Logic/unit]** +Dado un pool con `P = 2` y `draw_size = 3` (caso real justo tras la primera muerte), cuando se invoca F-RB3, entonces `offer` contiene exactamente 2 reliquias (2-choose-1) — nunca se duplica una reliquia para completar 3, nunca se lanza una excepción. + +**AC-RB20 — `P = 0` → loud-fail defensivo, nunca alcanzado en la práctica** **[Logic/unit]** +Dado un pool con `P = 0` invocado de forma forzada (bypaseando el gate de estado `READY`), cuando se invoca F-RB3, entonces falla ruidosamente en build de desarrollo — señal de bug de máquina de estados aguas arriba (Regla 9 debería haber impedido llegar aquí). + +**AC-RB21 — Las reliquias no elegidas vuelven íntegras al pool; solo la carga se gastó** **[Logic/unit]** +Dado una oferta de 3 reliquias donde el jugador elige 1, cuando la invocación resuelve, entonces las 2 no elegidas permanecen disponibles en el pool para sorteos futuros — ninguna se consume ni se marca como usada. + +**AC-RB22 — Ninguna oferta contiene más de `max_legendary_per_draw` Legendarias (Rule B)** **[Logic/unit]** +Dado un pool con ≥5 Legendarias y `max_legendary_per_draw = 1` (MVP), cuando se corre F-RB3 para **N = 1000 sorteos** con la lista de semillas fija `[1..1000]`, entonces **0 de los 1000** sorteos contienen más de 1 Legendaria — el test falla si **cualquier** sorteo individual excede el techo. *(Muestra y semillas fijas para determinismo reproducible; el pass/fail es binario: cero violaciones.)* + +**AC-RB23 — Determinismo: mismo seed + mismo pool → oferta idéntica** **[Logic/unit]** +Dado el mismo `rng_seeded` y el mismo estado de pool, cuando F-RB3 se invoca dos veces de forma independiente, entonces `offer` es idéntico en ambas invocaciones (mismos `relic_id`, mismo orden). + +### G. Ventana de invocación — solo `CLIMAX` (Regla 7) + +**AC-RB24 — La invocación está disponible mientras la fase es `CLIMAX`** **[Integration]** +Dado el Temporizador en fase `CLIMAX` con `invocation_charges_remaining > 0` y pool no vacío, cuando se evalúa `can_invoke`, entonces es `true` y el estado del sistema es `READY`. + +**AC-RB25 — La invocación se rechaza fuera de `CLIMAX`** **[Integration]** +Dado el Temporizador en fase `PREPARATION` o `IMMINENT`, cuando el jugador intenta invocar, entonces `can_invoke = false` y la acción se rechaza sin gastar carga ni sortear oferta — independientemente de cuántas cargas o reliquias haya disponibles. + +### H. Alcance: no posee combate/stats; modificadores de instancia, nunca mutación persistente (Regla 8) + +**AC-RB26 — Reliquias/Bendiciones nunca muta `CombatProfile`/`HeroDefinition`/`TroopDefinition` persistente** **[Logic/unit]** +Dado la superficie de escritura de Reliquias/Bendiciones hacia Combate, Héroes y Tropas, cuando se audita qué campos modifica, entonces nunca escribe sobre el recurso de tipo persistente (`CombatProfile`/`HeroDefinition`/`TroopDefinition`) — toda modificación vive exclusivamente en la capa de modificadores de instancia efímeros de este sistema. *(Guard arquitectónico análogo al Pilar 1 — se complementa con un check estático de CI, ver nota de testabilidad al final.)* + +**AC-RB27 — Todo bono se aplica como modificador de instancia temporal, keyeado por `(relic_id, unidad)`** **[Logic/unit]** +Dado una invocación resuelta, cuando se crea (o refresca) su modificador, entonces queda asociado a la instancia concreta del héroe/tropa buffeado — nunca a su tipo/definición — e identificado por la clave `(relic_id, unit_instance_id)`: si ya existe un modificador con esa clave se refresca su duración (AC-RB31); si no, se crea uno nuevo. El modificador nunca muta el recurso persistente del tipo (AC-RB26). + +### I. Sistema inerte con pool vacío (Regla 9) + +**AC-RB28 — Pool vacío → `READY` nunca se alcanza; invocación no se ofrece** **[Logic/unit]** +Dado un pool con 0 reliquias (antes de la primera muerte de héroe en el legado), cuando se entra a `CLIMAX`, entonces el sistema no alcanza `READY` y no se ofrece ningún affordance de invocación. + +**AC-RB29 — El pool vacío es onboarding natural, no un candado artificial ni un error** **[Logic/unit]** +Dado un pool vacío, cuando se audita el motivo por el que `can_invoke = false`, entonces la razón es exclusivamente "pool vacío" (Regla 9) — no existe ninguna bandera de "sistema bloqueado por progreso" separada; en cuanto Forja deposita la primera reliquia, `can_invoke` puede volverse `true` sin ninguna otra condición de desbloqueo. + +### J. Stat efectivo con modificadores temporales de instancia (Fórmula F-RB1) + +**AC-RB30 — `effective_stat = base_stat + Σ magnitude(m)`, aditivo, sin cap** **[Logic/unit]** +Dado un héroe con `base_stat(attack_damage) = 25` y dos modificadores activos de magnitud 12 y 8, cuando se evalúa F-RB1, entonces `effective_stat = 45` — sin ningún tope superior aplicado por este sistema. + +**AC-RB31 — Re-elegir una reliquia ya activa sobre la misma unidad refresca su duración; NO apila una segunda instancia** **[Logic/unit]** +Dado un héroe con un modificador activo de la reliquia X (aplicado en `t=1000`, expira en `t=2200`), cuando el jugador vuelve a elegir la reliquia X sobre ese mismo héroe en una invocación posterior (p. ej. en `t=1600`), entonces **no** se crea un segundo modificador — el modificador existente de X sobre ese héroe **refresca su `expiry_tick`** a una ventana completa nueva (`applied_tick` = tick actual → nuevo `expiry_tick`) y su magnitud permanece igual (mismo tier). El bono a ese stat por la reliquia X **nunca** excede una sola magnitud de X, sin importar cuántas veces se re-elija. *(Reliquias distintas sobre el mismo héroe sí mantienen modificadores independientes — ver AC-RB30.)* + +**AC-RB31b — Reliquias distintas del mismo stat apilan aditivamente; el bono total está acotado por la cota worst-case** **[Logic/unit]** +Dado un héroe con modificadores activos de **dos reliquias distintas** (X e Y) que modifican `attack_damage`, cuando se evalúa F-RB1, entonces ambas magnitudes suman (apilado entre reliquias distintas permitido) y el bono total nunca excede `max_stat_bonus(attack_damage)` según la cota computable de F-RB1 — verificado contra el peor caso MVP (3 Legendarias ofensivas distintas → +54). + +**AC-RB32 — Read-hook `effective_attack_damage(instancia)` de Combate/Daño** **(desbloqueado — ADR-0001, Accepted 2026-08-21)** **[Integration]** +*Contrato real (ADR-0001): Combate expone `CombatState.effective_attack_damage() → int` sobre la capa de modificadores por instancia; ya no es un mock. La API de escritura es `add_modifier(mod)`/`remove_by_source(source_relic_id)`, y Combate barre expirados con `sweep_expired(current_tick)`.* +Dado un héroe con modificadores activos, cuando Combate resuelve un golpe, entonces lee el valor ya capeado por `effective_attack_damage()` (no el `base_stat` crudo) — confirma el resultado de F-RB1 contra el contrato aceptado de Combate. + +### K. Expiración por tick (Fórmula F-RB4) + +**AC-RB33 — `duration_ticks = ceil(duration_s × physics_ticks_per_second)`, intervalo semiabierto** **[Logic/unit]** +Dado `duration_s = 20.0` y `physics_ticks_per_second = 60`, cuando se aplica un modificador en `applied_tick = 1000`, entonces `duration_ticks = 1200`, `expiry_tick = 2200`, y `is_active(t)` es `true` para `1000 ≤ t < 2200` y `false` en `t = 2200`. + +> **Fuente de tick (ADR-0002, Accepted 2026-08-21):** el contador `t`/`applied_tick`/`expiry_tick` es **`TimeControl.game_tick`** (entero, pause-aware y scale-aware), **no** `Engine.get_physics_frames()`. La fórmula `duration_ticks = ceil(duration_s × 60)` no cambia, pero como `game_tick` se congela en pausa y avanza escalado bajo `offering_time_scale`, las bendiciones duran **tiempo de juego** (se ralentizan/congelan con el régimen), no wall-clock. El barrido de expiración lo ejecuta Combate (`sweep_expired(game_tick)`, ADR-0001) al inicio del tick, antes de la fase-1 de daño. + +**AC-RB34 — La expiración se chequea al inicio del tick, antes de la fase-1 de daño de Combate** **[Integration]** +Dado un modificador con `expiry_tick = t`, cuando el tick `t` comienza, entonces el modificador ya fue removido **antes** de que Combate resuelva cualquier `apply_damage` de ese tick — ningún golpe del tick `t` puede leer un modificador que expiró justo en `t`. + +**AC-RB35 — Los efectos instantáneos omiten el bookkeeping de ticks y van directo a `SPENT`** **[Logic/unit]** +Dado un efecto `rally` (instantáneo), cuando se resuelve en `RESOLVING`, entonces transiciona directamente a `SPENT` sin pasar por `ACTIVE` ni registrar `applied_tick`/`expiry_tick`. + +**AC-RB36 — Expiración de vida máxima defensiva: clamp únicamente, nunca vía `apply_damage`, nunca mata a un héroe** **[Integration]** +Dado un héroe con `max_health` elevado `+N` por un modificador defensivo activo y `current_health` cercano al nuevo máximo, cuando el modificador expira, entonces `max_health` baja `−N` y `current_health = min(current_health, nuevo_max_health)` mediante clamp directo — la expiración nunca invoca `apply_damage`, nunca dispara `DYING`, y `current_health` resultante nunca es ≤ 0 por efecto de la expiración en sí. + +### L. Composición del pool activo — tope + retiro FIFO (Fórmula F-RB5) + +**AC-RB37 — `active_pool_size = min(eligible_relics_count, pool_cap)`, con `eligible_relics_count = count(origin_era_id ≠ active_era_id)` (exclusión intra-era, Regla 10)** **[Logic/unit]** +Dado 10 reliquias forjadas, de las cuales **8 tienen `origin_era_id ≠ active_era_id`** (2 fueron forjadas en la era ACTIVE) y `pool_cap = 16`, cuando se evalúa F-RB5, entonces `eligible_relics_count = 8` y `active_pool_size = 8` — las 2 reliquias intra-era quedan **excluidas** del pool invocable (Regla 10). Dado 20 elegibles con el mismo `pool_cap`, entonces `active_pool_size = 16` (tope FIFO sobre las elegibles, por `forge_index`). Dado 0 elegibles (todas las forjadas son de la era ACTIVE), entonces `active_pool_size = 0` y el estado `READY` nunca se alcanza (AC-RB28). + +**AC-RB38 — Al exceder `pool_cap`, la reliquia de menor `forge_index` se retira del pool activo (flag efímero); la `RelicRecord` nunca se borra** **[Logic/unit]** +Dado un pool activo en su tope (`pool_cap = 16`) y una nueva reliquia forjada (`forge_index` 17º), cuando se deposita, entonces la reliquia con el menor `forge_index` entre las activas cambia su estado efímero "en pool de sorteo" a `false` — su `RelicRecord` en el registro de Forja permanece intacta y sin borrar; la nueva reliquia entra al pool activo. + +**AC-RB39 — La reliquia retirada se exhibe en el Panteón como solo-display** **(bloqueado — Salón Conmemorativo/Panteón #14 sin GDD)** **[Integration]** +*Mock Contract Assumption: el mock de Panteón expone un único hook de lectura `list_retired_relics() → [RelicRecord]` para las reliquias marcadas fuera del pool activo. Re-verificar contra el GDD real de Salón Conmemorativo/Panteón cuando exista.* +Dado una reliquia recién retirada del pool activo (AC-RB38), cuando el mock de Panteón consulta `list_retired_relics()`, entonces la recibe con su identidad completa (nombre, categoría, tier, epitafio, era) — confirma que la mitad de "retirada = consagrada" (exhibición) tiene una superficie mínima ya expuesta, distinta de la mitad de retiro-del-pool (AC-RB38, testeable hoy sin mock). + +### M. Edge cases de anti-abuso y coordinación cross-sistema + +**AC-RB40 — La carga se gasta incluso si el efecto resulta en no-op/whiff** **[Integration]** +Dado un efecto `rally` invocado cuando la banda ya está en su tope de unidades (`band_headroom == 0`, donde `band_headroom = max(0, max_band_size − alive_troop_count)`; `max_band_size` autorado por Datos de Era, enforcement en Tropas Fórmula E), cuando la invocación resuelve, entonces el refuerzo surte `min(N, 0) = 0` (no-op) pero `invocation_charges_remaining` se decrementa igual — la carga se gasta al elegir, no al surtir efecto (cross-ref Tropas AC-33). + +**AC-RB41 — Un buff activo se descarta si el héroe muere antes de expirar; no altera la clasificación D/T/P** **[Integration]** +Dado un héroe con un modificador ofensivo activo que entra a `DYING` antes de que el modificador expire, cuando Héroes procesa la transición, entonces el modificador se descarta junto con la instancia del héroe (no se transfiere, no sobrevive) y Combate clasifica la muerte (`D`/`P`) sin que el bono de daño altere esa clasificación. + +### N. Invariantes loud-fail (carga) + +**AC-RB42 — `draw_size ≥ 1` y `invocation_charges ≥ 1`** **[Logic/unit]** +Dado una configuración con `draw_size = 0` o `invocation_charges = 0`, cuando se carga, entonces falla ruidosamente en build de desarrollo — ninguno de los dos puede ser 0 (un sorteo de tamaño 0 no es elección; 0 cargas es un sistema inerte por config, no por diseño). + +**AC-RB43 — `duration_s > 0` para todo efecto temporizado** **[Logic/unit]** +Dado `effect_duration_s = 0` o negativo para un efecto ofensivo/defensivo (temporizado), cuando se carga, entonces falla ruidosamente — un efecto temporizado no puede tener duración nula o negativa. + +**AC-RB44 — `solid_factor > 1.0` y `legendary_factor > solid_factor`** **[Logic/unit]** +Dado `solid_factor = 1.0` (o `legendary_factor ≤ solid_factor`), cuando se carga, entonces falla ruidosamente — rompería la monotonía de tier que AC-RB11 protege en tiempo de ejecución. + +**AC-RB45 — `base_value ≥ 1`; `pool_cap ≥ 1`; `0 ≤ max_legendary_per_draw ≤ draw_size`** **[Logic/unit]** +Dado cualquier `*_base` en 0, `pool_cap = 0`, o `max_legendary_per_draw` fuera de `[0, draw_size]`, cuando se carga la configuración, entonces falla ruidosamente en build de desarrollo — ninguno de estos límites se clampa en silencio. + +### O. Persistencia del estado efímero de invocación (recarga a mitad de era) + +**AC-RB46 — Cargas gastadas y modificadores activos se restauran en una recarga a mitad de era; las cargas gastadas nunca se devuelven** **(bloqueado — contrato de persistencia a coordinar con Guardado, OQ-RB2)** **[Integration]** +*Mock Contract Assumption: el mock de Guardado expone `save_ephemeral_invocation_state(charges_remaining, active_modifiers)` / `load_ephemeral_invocation_state() → {charges_remaining, active_modifiers}` como el contrato provisional de round-trip. Re-verificar contra la coordinación real con Guardado cuando OQ-RB2 se resuelva.* +Dado un estado con `invocation_charges_remaining = 1` (de 3) y un modificador activo con `expiry_tick` restante, cuando el juego recarga desde el mock de Guardado, entonces `invocation_charges_remaining` se restaura en `1` (no vuelve a 3 — las cargas gastadas no se devuelven por recargar, decisión de diseño anti-abuso ya fijada) y el modificador activo se restaura con su tiempo de expiración restante correctamente calculado. + +### Clasificación de tipo de story (gate de evidencia) + +| AC # | Tipo | Evidencia | Gate | +|---|---|---|---| +| AC-RB02, RB03–RB13, RB15, RB15b, RB16, RB18–RB23, RB26, RB27–RB31, RB31b, RB33, RB35, RB37, RB38, RB42–RB45 | Logic | Unit test (`tests/unit/reliquias-bendiciones/`) | Bloqueante | +| AC-RB01, RB14, RB17, RB24, RB25, RB34, RB36, RB40, RB41 | Integration | Integration test | Bloqueante | +| AC-RB32 | Integration | Integration test contra el contrato real de Combate/Daño (read-hook `effective_attack_damage` confirmado por ADR-0001, Accepted) | Bloqueante (pendiente solo de implementación) | +| AC-RB39 | Integration (bloqueado) | Integration test con mock (Salón Conmemorativo/Panteón #14 — sin GDD) | Bloqueante (hoy solo vía mock) | +| AC-RB46 | Integration (bloqueado) | Integration test con mock (Guardado — contrato de estado efímero a coordinar, OQ-RB2) | Bloqueante (hoy solo vía mock) | + +*(Nota: ningún AC de Reliquias/Bendiciones depende de Forja de Legado, Temporizador, Sistema de Tropas, Sistema de Héroes, Datos de Era/Civilización o Guardado/Persistencia estando "sin GDD" — los seis ya tienen GDD, así que esos AC son Integration normal, pendientes solo de implementación. AC-RB32 quedó **desbloqueado** por ADR-0001 (Accepted). Quedan bloqueados solo RB39 y RB46, por dos contratos abiertos: #14 [Panteón, sin GDD], OQ-RB2 [Guardado].)* + +**Nota de testabilidad (checks estáticos de CI que complementan los unit tests):** + +- **Regla 1 ("nunca forja/modifica/borra una reliquia") y Regla 8 ("nunca muta `CombatProfile`/`HeroDefinition`/`TroopDefinition` persistente")** son afirmaciones de superficie/ownership, no de runtime — no hay ningún input de "escritura sobre Forja" o "mutación de un `Definition` persistente" que este sistema pueda rechazar en caja negra, porque esa ruta de código simplemente no debe existir. AC-RB02 y AC-RB26 las cubren como auditoría de superficie; se complementan con un **grep estático de CI** sobre el módulo de Reliquias/Bendiciones que verifica que ningún símbolo de escritura de Forja (`forge_relic`, `delete_relic`, etc.) ni ninguna asignación directa a un campo de `CombatProfile`/`HeroDefinition`/`TroopDefinition` aparece fuera de la capa de modificadores de instancia efímeros. *(Target y ruta exactos del módulo a fijar en el futuro ADR de este sistema, mismo patrón que Forja OQ-FL4.)* + +## Open Questions + +**OQ-RB1 — ✅ RESUELTO 2026-08-21 — ADR-0001 (Accepted).** +Los efectos ofensivos requerían que Combate expusiera `effective_attack_damage(instancia)` capando los modificadores de instancia sobre el `CombatProfile` del tipo (F-RB1). **ADR-0001 (`docs/architecture/adr-0001-combat-stat-modifier-layer.md`, Accepted 2026-08-21) lo resolvió**: Combate posee una capa `CombatState` por instancia con `effective_attack_damage()` (read-hook) y `add_modifier`/`remove_by_source`/`sweep_expired` (API de escritura que este sistema llama). La expiración vive en Combate y se ancla a `TimeControl.game_tick` (ADR-0002). Desbloquea AC-RB32. + +**OQ-RB2 — Persistencia del estado efímero de invocación (contrato con Guardado a coordinar).** +El pool (panteón) lo persiste Forja; pero las **cargas gastadas** y los **modificadores activos con su tick de expiración restante** deben restaurarse en una recarga a mitad de era, sin devolver cargas gastadas (anti-abuso, decisión fijada). La forma exacta del round-trip (qué se serializa, cómo se recalcula el tiempo restante de un modificador a través de una recarga) es un contrato a coordinar con `guardado-persistencia.md`. **Acción**: confirmar el contrato con Guardado; bloquea AC-RB46. + +**OQ-RB3 — ✅ CASI RESUELTO 2026-08-21 (reconciliación cross-GDD).** +La fila de `systems-index.md` ya lista las 8 dependencias upstream. Las **filas recíprocas de dependiente** que faltaban se añadieron en la reconciliación cross-GDD del 2026-08-21: **Temporizador**, **Sistema de Tropas**, **Sistema de Héroes** y **Datos de Era/Civilización** ahora reconocen a Reliquias/Bendiciones como dependiente. **Pendiente solo la fila de `guardado-persistencia.md`** — diferida al ADR de Save/Persistencia (ADR-0003, en curso), que reconcilia toda la superficie de persistencia de este sistema (estado efímero de invocación, OQ-RB2). Cosmético/estructural, no cambia diseño. + +**OQ-RB4 — Ventana de invocación en `PREPARATION` (afinación).** +El MVP arranca con invocación solo-`CLIMAX` (Regla 7). Queda abierto si conviene permitir bendiciones defensivas/rally de *preparación* durante `PREPARATION` (posicionar antes del clímax) — el core loop del concepto menciona "decidir cuándo invocar" durante el posicionamiento. **Acción**: afinar en Pre-Producción/vertical slice tras playtest; el MVP valida solo-`CLIMAX` primero por simplicidad. + +**OQ-RB5 — Forma exacta del panteón de prueba pre-sembrado (política ya decidida).** +**RESUELTO (política): el pool se puebla SOLO de reliquias de eras anteriores; nunca de muertes intra-era** (Regla 10). La cadena de legado es cross-era por naturaleza (muerte en era N → reliquia usable en era N+1); una muerte durante el CLIMAX en curso forja su reliquia pero no la vuelve draftable en la misma pelea (preserva el peso trágico y la curva de onboarding "las bendiciones no se exponen en la 1ª partida"). Para el MVP de una sola era esto obliga a **pre-sembrar un panteón de prueba** (roster de muertes de una "era 0" ficticia) — de lo contrario el pool arranca vacío y el sistema es inerte toda la partida. **Queda abierto solo el contenido del pre-sembrado**: cuántas reliquias (la MVP Definition del concepto pide "2-3 reliquias heredables"), y qué mezcla de `relic_category`×`tier` demuestra mejor el hook (idealmente ≥1 de cada categoría y ≥1 tier alto para exhibir la escalera). **Acción**: fijar el contenido del panteón de prueba al poblar el save inicial de la primera era; coordinar con Datos de Era/Civilización (roster) y con la hipótesis central del MVP. *(La política de "solo eras anteriores" ya no es una pregunta abierta.)* + +**OQ-RB6 — Representación e implementación (→ ADR; parcialmente resuelto).** +**Ya decidido por ADR-0001/0002 (Accepted 2026-08-21):** la estructura del stack de modificadores de instancia (`StatModifier`/`CombatState`, dueño Combate) y la fuente de tick de expiración (`TimeControl.game_tick`, pause/scale-aware). **Queda abierto** para el ADR propio de este sistema: dónde vive el estado efímero de invocación en runtime (Autoload vs. nodo), el algoritmo exacto del techo de Legendaria por sorteo (sub-pool ponderado vs. reroll de exceso, Rule B/F-RB3), y el patrón de inyección del RNG semillado. **Acción**: crear ese ADR con `/architecture-decision` antes de implementar, coordinado con el ADR de Forja (OQ-FL4/FL5). + +**OQ-RB7 — API exacta de refuerzo `rally` (firma) — tope de banda ✅ RESUELTO 2026-08-21 (F2-B5).** +El efecto `rally` invoca la API de spawn/reclutamiento de Sistema de Tropas; el comportamiento en tope de banda ya está decidido (no-op parcial, carga igual se gasta — AC-RB40). **La relación con el tope de banda quedó resuelta** (Fórmula E de `sistema-de-tropas.md`): el tope de tropas simultáneas es `max_band_size`, un valor **autorado por era** (propiedad de Datos de Era/Civilización); Tropas hace el enforcement y expone `band_headroom = max(0, max_band_size − alive_troop_count)`, read-only para este sistema. `rally` surte `spawned = min(rally_N, band_headroom)` tropas y nunca empuja las unidades vivas por encima de `max_band_size`; si `band_headroom == 0` es no-op (AC-RB40). Es un grifo independiente del presupuesto por-tipo/por-ciclo de PREPARATION (`max_reinforcements_[tipo]`); ambos respetan `max_band_size` como techo total (cierra F2-B5). **Queda abierto solo** la firma exacta de la API de spawn. **Acción**: confirmar la firma al implementar, contra `sistema-de-tropas.md` (Fórmula E, AC-32/33/34). + +**OQ-RB8 — Validaciones de balance diferidas a vertical slice (economy-designer).** +Cuatro cosas quedan pendientes de validar contra datos que aún no existen: (a) el buff de banda `rally` contra el tamaño típico de banda en `CLIMAX` (sus números 2/3/5 son provisionales y no cross-validables hasta tener ese dato); (b) los efectos ofensivos/AoE contra los `CombatProfile` de esbirros (era-definidos, sin valores MVP aún); (c) `effect_duration_s=20` contra el timing real del ciclo del kaiju; (d) **dominancia cross-categoría** — medir la tasa de elección ofensivo vs. defensivo vs. `rally`; si ofensivo domina independientemente del contexto (valor incondicional > contingente), corregir subiendo el piso de valor del defensivo (p. ej. la mitigación plana diferida) o bajando el ofensivo (ver "Anclaje cross-categoría" en Formulas). **Acción**: afinar en vertical slice con telemetría/playtest; (d) es la validación que decide si el anclaje cross-categoría del MVP fue suficiente. + +**OQ-RB9 — ✅ RESUELTO 2026-08-21 — ADR-0002 (Accepted).** +La cámara lenta de la oferta (`offering_time_scale`, Regla 7b) ralentiza la simulación completa (kaiju, telegrafío, unidades) de forma uniforme. **ADR-0002 (`docs/architecture/adr-0002-time-control-service.md`) lo resolvió**: el dueño del régimen de tiempo de juego es el servicio Core **TimeControl**; Reliquias/Bendiciones **solicita** el régimen `SLOWED` con `offering_time_scale` vía `request_regime(source_id, SLOWED)` mientras `OFFERING` está abierto, y lo libera al cerrar/cancelar — **no posee** el reloj. TimeControl NO usa `Engine.time_scale` ni `SceneTree.paused`: los consumidores escalan su delta por `time_scale` por polling. La expiración por tick (F-RB4) sigue correcta porque `TimeControl.game_tick` es un contador entero scale-aware. Exentos de la ralentización (corren en tiempo real): la UI de la oferta, el HUD, y el reloj del beat `DEATH_HOLD`. **Interacción con la pausa de muerte de Permadeath (`game_time_paused`) — precedencia ratificada como contrato.** Permadeath Regla 4 detiene el tiempo por completo (`game_time_paused=true`, input suprimido) en la muerte de un héroe. Si un héroe muere **durante** un `OFFERING` abierto (edge case reconocido), la **pausa total de Permadeath tiene precedencia** sobre la cámara lenta de la oferta: el `offering_time_scale` se suspende mientras dure la pausa de muerte, la oferta se **preserva** (no se cancela ni corre su soft-timeout durante la pausa), y reanuda su cámara lenta cuando la pausa de muerte termina. Razón: la muerte es el beat emocional mayor (Pilar 1) y Permadeath ya posee un stop total; anidar dos regímenes de tiempo con la muerte ganando es lo coherente. **Acción**: coordinar con technical-director/Temporizador/Combate **y Permadeath** (añadir Reliquias a la lista de consumidores de `game_time_paused` de Permadeath Regla 4); capturar en el ADR de patrón de este sistema (OQ-RB6). Bloquea la implementación de la Regla 7b (no el diseño). + +**OQ-RB10 — Individuación de reliquias más allá de las 9 combinaciones categoría×tier (coordinación con Forja).** +El efecto y el `epitaph` de una reliquia derivan solo de `relic_category` × `tier` (9 combinaciones; `epitaph` de una tabla-plantilla de 9 entradas owned por Forja, AC-FL09). El canal glanceable (retrato + nombre del héroe, RV-2) individualiza la oferta en el momento de elegir, lo que **satisface el design-test mínimo** ("no intercambiable con un drop genérico") — pero dos reliquias del mismo categoría×tier siguen siendo mecánica y textualmente idénticas salvo por quién las forjó. Individuación más profunda (epitafios que citen la circunstancia específica de la muerte: qué kaiju, qué acción, a quién protegía) **requiere extender el dato que posee Forja**, no este sistema. **Acción**: recomendación cross-GDD para Forja de Legado — evaluar si el `epitaph` debe incorporar detalle por-muerte (más allá de la plantilla categoría×tier) para reforzar el Pilar 2. No bloquea el MVP (el canal glanceable basta para el design-test), pero es la vía para que el sistema supere su propio test con holgura, no al mínimo. diff --git a/design/gdd/reviews/combate-dano-review-log.md b/design/gdd/reviews/combate-dano-review-log.md new file mode 100644 index 0000000000..a1252f6f0b --- /dev/null +++ b/design/gdd/reviews/combate-dano-review-log.md @@ -0,0 +1,42 @@ +# Review Log — Combate/Daño + +## Review 1 — 2026-08-09 — Verdict: NEEDS REVISION → fixes aplicados → Approved +Scope signal: L (multi-system integration — Tropas, Héroes, Control, provisional Kaiju/Permadeath; 5 fórmulas; ≥2 ADRs probables — CombatProfile storage OQ-1, mecanismo loud-fail) +Specialists: game-designer, systems-designer, qa-lead, ai-programmer, godot-specialist + creative-director (synthesis) +Blocking items: 5 (todos resueltos en esta sesión) | Recommended: ~11 (diferidos a backlog) | Nice-to-have: ~5 + +### Hallazgos y resolución +**BLOCKING (5/5 resueltos en esta sesión):** +- **B1 [godot-specialist, promovido a bloqueante por creative-director] — sin garantía de orden para el freeze de D/P "en el mismo frame".** Regla 7/8 depende de que D/P se congelen contra un instante coherente de cruce por 0, pero nada especificaba el orden de resolución dentro de un tick — una implementación ingenua podría disparar `DYING` de un héroe antes de que el golpe letal simultáneo de otro se aplicara, rompiendo en silencio la garantía de mutual-kill (AC-C42). **Fix:** Regla 1 ahora exige resolución en dos fases estrictas por tick — (1) aplicar todo el daño pendiente, (2) solo entonces disparar notificaciones de muerte y computar D/P. +- **B2 [godot-specialist] — loop de combate sin especificar (reproducibilidad de TTK).** Los TTK exactos de F3 (AC-C21–C24) solo son reproducibles si el tick corre en paso fijo; en `_process` serían framerate-dependientes. **Fix:** Regla 1 fija el tick de combate a `_physics_process`; nota de convención en F3 actualizada. +- **B3 [systems-designer] — contradicción AC-C05 vs AC-C43 (cooldown).** AC-C43 (el cooldown persiste tras perder/cambiar objetivo) y AC-C05 (un objetivo recién adquirido siempre espera un cooldown completo) quedaban indefinidos en su intersección — abría un exploit de "banking" de cooldown en `NO_TARGET`. **Fix (resolución de creative-director, confirmada por el usuario):** mecánica de doble compuerta — el cooldown heredado nunca se resetea (compuerta 1) y además se aplica un "piso de adquisición" que nunca permite un golpe antes de un `attack_cooldown_s` completo desde la adquisición (compuerta 2). Regla 3 reescrita; AC-C05/AC-C43 amendadas; **AC-C43b** nueva prueba el exploit directamente. +- **B4 [systems-designer] — invariante F5 no satisfacible para el Kaiju.** Enforcement exigía loud-fail uniforme sobre `attack_range ≤ aggro_range ≤ leash_range`, pero el `leash_range` del Kaiju es un centinela `provisional*` sin valor real — el juego no podía arrancar. **Fix (confirmado por el usuario):** excepción nombrada explícita — F5 no evalúa el tramo `≤ leash_range` del Kaiju mientras sea centinela; el resto del invariante sigue con loud-fail normal. Añadido a Formulas F5, Enforcement, Tuning Knobs y OQ-7; **AC-C27b** nueva prueba el límite de la excepción. +- **B5 [ai-programmer] — "posición de retención" (ancla de leash) sin definir.** Regla 4/AC-C07/AC-C44 medían distancia de leash contra ella sin decir cuándo se fija — afectaba materialmente cuánto puede derivar una unidad de su formación en cadenas de kills. **Fix (confirmado por el usuario):** se fija en el instante de entrada a `TARGET_ACQUIRED`, no se actualiza durante la persecución, y se re-fija en cada nueva adquisición (incluida tras encadenar un kill). Regla 4 y States/Transitions actualizadas; **AC-C07b** nueva la prueba. + +**Decisiones del usuario (desbloquearon B3, B4, B5):** las tres resoluciones recomendadas por creative-director/specialists fueron confirmadas explícitamente vía pregunta multi-opción antes de aplicar cualquier edición — doble compuerta para cooldown (no reset-on-switch ni instant-on-stale), excepción nombrada (no valor centinela numérico) para F5/Kaiju, y ancla fija-en-adquisición (no pinned-a-idle ni continuamente actualizada) para el leash. + +**Disagreement adjudicado por creative-director:** severidad del riesgo de "mismo frame" — qa-lead lo calificó MINOR/no-bloqueante (nota de test-flakiness); godot-specialist lo calificó IMPORTANT (riesgo de corrección, no solo de test). Creative-director confirmó a godot-specialist y lo promovió a bloqueante (B1) — la garantía de "congelado en ese mismo frame" es la columna mecánica del Pilar 1 en este sistema. + +**RECOMMENDED diferidos a backlog (no bloqueantes, no aplicados en esta sesión):** +- [game-designer, adjudicado por creative-director] Sacrificio sin costo hace D=1 trivialmente declarable — escalar a OQ-2 con la restricción explícita "Sacrificio debe llevar un costo de declaración pagado antes de conocer el resultado" (ownership de Control/Permadeath, no de este clasificador). *Aún no aplicado a OQ-2 — pendiente.* +- [systems-designer] Sin cota superior de enforcement para `attack_damage`/DPS. +- [ai-programmer] Desempate de "más cercano" (AC-C06) sin regla determinista para empates exactos. +- [ai-programmer] Stickiness de re-adquisición sin especificar (¿una unidad ya enganchada re-evalúa por un enemigo más cercano que entra en rango?). +- [systems-designer] Ownership de `health_current` como split-brain — merece su propia Open Question, paralela a OQ-1. +- [qa-lead] AC-C-NEG no especifica mecanismo de verificación (schema vs. valor) — riesgo de passing gameable. +- [qa-lead] AC-C33 y la mitad bloqueada de AC-C42 sin Mock Contract Assumptions, a diferencia de sus pares AC-C32/AC-C52. +- [qa-lead] AC-C41 solo prueba rechazo de daño, no el segundo mecanismo del edge case (aliados nunca son objetivo válido de aggro). +- [ai-programmer] Riesgo de `queue_free()` diferido — un objetivo "muerto" podría seguir pasando checks de rango el resto del frame. +- [ai-programmer] Chequeos de leash/aggro/attack puramente por distancia (sin pathfinding) — riesgo de persecuciones rotas visualmente; falta como Open Question. +- [main review] Nota stale en `sistema-de-heroes.md` AC-H21 ("sin GDD" para Combate/Daño) — ya no es precisa, ahora existe este GDD; el bloqueo real es OQ-2 de Control. Fix pertenece a ese archivo, no a este. + +**NICE-TO-HAVE (sin aplicar):** +- [ai-programmer] FSM de un-solo-objetivo-por-combatiente sin hook para cleave/AOE futuro del Kaiju. +- [systems-designer] Matriz de TTK implícitamente 1v1, sin advertencia sobre combate grupal. +- Formato de decimales de DPS inconsistente (2 decimales tropas, 1 decimal héroes/kaiju). +- AC-C42 no marcada `(bloqueado)` en la columna Tipo de la tabla de gating, solo en Evidencia. +- Ítem de acción ya completado ("Nota cross-GDD" de HP en `sistema-de-tropas.md`) — ese archivo ya muestra "✅ RESUELTO 2026-08-09". + +### Estado final +Verdict: **APPROVED** (tras revisión — usuario aceptó sin re-review completo). `systems-index.md` actualizado (Combate/Daño: Designed → Approved). +Prior verdict resolved: N/A — primera revisión de este documento. diff --git a/design/gdd/reviews/encuentro-con-kaiju-review-log.md b/design/gdd/reviews/encuentro-con-kaiju-review-log.md new file mode 100644 index 0000000000..65d42670cc --- /dev/null +++ b/design/gdd/reviews/encuentro-con-kaiju-review-log.md @@ -0,0 +1,85 @@ +# Review Log — Encuentro con Kaiju + +Historial de revisiones de diseño de `design/gdd/encuentro-con-kaiju.md`. + +## Review — 2026-08-11 — Verdict: MAJOR REVISION NEEDED +Scope signal: L–XL +Specialists: game-designer, systems-designer, ai-programmer, qa-lead, level-designer, godot-specialist, ux-designer, creative-director (síntesis senior) +Blocking items: 9 | Recommended: 7 | Disagreements adjudicados: 2 +Prior verdict resolved: First review + +**Resumen (síntesis del creative-director)**: La artesanía del documento es fuerte (ciclo de intención, marcado-vía-telegrafío, disciplina de contratos cross-sistema), pero tres de los cuatro pilares tenían un hueco bloqueante y había un riesgo de integridad cross-GDD. Verdict estructural, no de pulido; los arreglos son aditivos. + +**9 items bloqueantes identificados:** +1. [game-designer] El exploit de matar solo con tropas esquiva el Pilar 1 (los héroes son la única condición de derrota pero nada obliga a arriesgarlos). +2. [systems-designer] Los stats "locked" del kaiju (`attack_damage=45`/`attack_cooldown_s=4.5`) NO están locked en Combate/Daño — son knobs tunables; AC-H29 (~30s) depende de ellos sin trigger de re-verificación. +3. [ux/level/systems] `time_to_death` proyecta contando solo el kaiju; daño de esbirros simultáneo lo vuelve silenciosamente erróneo en la dirección fatal (viola Pilar 4). +4. [level-designer] Acumulación no acotada de esbirros (hasta 48, sin despawn/cap) + sin cota de duración del clímax (Temporizador OQ-4 se difirió aquí y quedó sin responder). +5. [systems-designer] El borde del rango seguro de F4 rompe la propiedad "una tropa sola no neutraliza" (acoplamiento `neutralize_hits_equivalent × telegraph_duration_s` no documentado). +6. [systems-designer] La derivación `telegraph_duration_s ≥ 12.0s` de F2 no es robusta frente al rango de HP válido de Héroes `[120,200]` (un héroe de 120 HP rompe AC-H29). +7. [ai-programmer] El auto-ataque del FSM base de Combate/Daño nunca se suprime durante el telegrafío (el kaiju golpearía antes/durante su propio telegrafío). +8. [ai-programmer] La tabla States and Transitions omite la transición de telegrafío cancelado (objetivo muerto/fuera de rango) que Edge Cases/AC-K06/K46 exigen. +9. [qa-lead] Los mocks de AC-K58/K59 no prueban sus contratos documentados (falta `time_to_death` en el payload; falta parámetro de tercio para KA-1). + +**Desacuerdos adjudicados por el usuario (2026-08-11):** +- Screening de tropas: NO es una capa táctica del MVP (retirada binaria intencional; "el kaiju viene por ti"). Revisar tras playtest (OQ-8). +- Ventana de telegrafío (¿demasiado fácil vs. demasiado corta?): diferido a tuneo de playtest; piso de aviso fiable fijado en el tercio 2 + canal numérico desde el frame 1 (nota KV-1). + +**Decisiones de diseño elegidas (recomendadas):** +- Anti-turtle + cota de duración → **Furia/enrage** (Regla 10, F5): dispara por tiempo (`enrage_time_threshold_s=150`) o por desenganche de héroes (`hero_disengage_window_s=15`); escala telegrafío y daño; latcheada. +- Honestidad de daño → **`incoming_damage_rate_total`** (Regla 6b, F6): expone DPS combinado de todas las fuentes; el readout de Héroes refleja la fuente más peligrosa. +- Concurrencia de esbirros → **`max_concurrent_esbirros`** (Regla 8b): las oleadas debidas se difieren al llegar al cap. + +**Revisión aplicada en la misma sesión (todos los bloqueantes + recomendados):** Reglas 4 (gate de ataque)/6b/8b/10 nuevas; F5/F6 nuevas; ACs añadidos AC-K61–K73; AC-K01/K05/K14/K17 divididos; AC-K58/K59 mocks enriquecidos; F1 algoritmo de ventana deslizante; RNG inyectable + framerate-independencia (AC-K70/K71); nota `@export_range` (loud-fail, no clamp); KV-9/KA-6 (Furia); reencuadre de stats "locked" (F2) + OQ-10/OQ-11/OQ-12; OQ-3 resuelto. + +**Nota cross-GDD pendiente para el re-review / consistency-check:** +- OQ-10: formalizar el lock o trigger de re-verificación de `attack_damage`/`attack_cooldown_s` del kaiju en `combate-dano.md` (no editado en esta sesión — manejado como constraint documentado en este GDD). +- OQ-4 (acción Phase 5 original): des-flaggear las notas "Encuentro con Kaiju sin GDD" en Combate/Daño y Héroes y re-verificar sus mocks contra el contrato real. + +**Estado tras la revisión:** GDD revisado, `Status: In Design (pendiente re-review)`. systems-index.md sin cambios (sigue "Designed", no "Approved") — a la espera de un re-review con contexto limpio (7 agentes). + +## Review — 2026-08-12 — Verdict: NEEDS REVISION → revisado en la misma sesión +Scope signal: L (sistema); M (la revisión) +Specialists: game-designer, systems-designer, ai-programmer, qa-lead, level-designer, godot-specialist, ux-designer, creative-director (síntesis senior) +Blocking items: 6 | Recommended: 8 | Prior verdict resolved: No (parcial — ver abajo) + +**Resumen (síntesis del creative-director)**: Doc genuinamente mejorado — 5 de los 9 items previos sólidamente cerrados y la disciplina de AC es la más fuerte del proyecto — pero 4 especialistas encontraron cada uno un bloqueante de *coherencia* (no de pulido), más 2 semi-bloqueantes. Meta-patrón central: los "arreglos" previos de los items 4/5/6 **reetiquetaron** huecos como "riesgo de disciplina de autor" en prosa en lugar de cerrarlos con el loud-fail que este GDD exige en todas partes — y 2 de esos 3 eran *computables*, así que declinar enforcement era el problema real. qa-lead habría aprobado (revisa testabilidad, no coherencia). + +**6 items bloqueantes identificados (y resueltos en la misma sesión):** +1. [ai-programmer] Contradicción del golpe de `STRIKING` vs. piso de adquisición del FSM base de Combate/Daño (el primer golpe se retrasaría un cooldown o saltaría el gate sin documentar). → Override explícito en Regla 4 + tabla de estados + AC-K75. +2. [systems-designer] La garantía ≥30s (F2/AC-H29) se rompe con valores individualmente válidos de dos GDDs Approved (`telegraph_duration_s=14.0` default + `hero_max_health=120`); enforcement era solo prosa. → Loud-fail obligatorio al cargar `EraDefinition` (OQ-11 resuelto) + AC-K74. +3. [systems-designer] Falso positivo del disparador (b) de Furia en el piso `hero_disengage_window_s=8` (retirada táctica legítima latchea Furia). → Piso subido 8→15 + edge case + AC-K63d. +4. [level-designer] El enrage es escalada, no cota de duración; la duración total del clímax sigue sin techo. → Corregida la afirmación imprecisa; cota dura total **aceptada conscientemente como riesgo documentado post-MVP** (decisión del usuario, OQ-12). +5. [game-designer] El disparador (b) permitía "héroe-cebo único" (cumple la letra del anti-turtle, rompe el Pilar 1). → Disparador consciente del roster + knob `hero_engagement_fraction` (decisión del usuario) + AC-K63/K63b/K63c. +6. [ux-designer] KV-1 apoyaba la garantía de Pilar 4 en un número del HUD; faltaba límite de fotosensibilidad y contrato de cancelación. → KA-1 tercio-1 como backstop diegético + KV-10 (flash-rate) + KU-1 (razón de cancelación). + +**Decisiones del usuario (2026-08-12):** (a) Furia anti-cebo → disparador consciente del roster; (b) cota de duración → aceptar como riesgo documentado post-MVP; (c) piso de desenganche → subir a 15s. + +**Recomendados aplicados:** `_validate_property`→setter custom/`validate()` (godot-specialist, prerrequisito de los loud-fail); polling directo para el gate de dos FSM; reencuadre de copy de `P` defensiva (Player Fantasy — no hay redirect vivo del kaiju); F6 rama `kaiju_projected_dps=0` (edge case + AC-K78); AC-K76 (framerate de ventana F1 + contador de neutralización); AC-K77 (timing de tercios, mueve el "medio timing" de Pilar 4 a gate blocking); techo de `max_concurrent_esbirros` 24→16 recomendado; regla de autoría `first_wave_delay_s ≥ telegraph_duration_s`; OQ-13/OQ-14 (audio tercio-1 / histéresis dual). + +**No accionados (pertenecen a otros docs):** OQ-10 (formalizar lock/trigger de los stats del kaiju en `combate-dano.md`); des-flaggeo cross-GDD de notas "sin GDD" stale en Combate/Héroes/Temporizador (acción de OQ-4). + +**Estado tras la revisión:** GDD revisado a 2026-08-12, `Status: In Design (pendiente re-review con contexto limpio)`. systems-index.md sin cambios (sigue "Designed"). Próximo paso recomendado por el usuario: **re-review en sesión nueva tras `/clear`** (7 agentes, contexto limpio) → si pasa, marcar Approved. + +## Review — 2026-08-12 (3er re-review) — Verdict: NEEDS REVISION → revisado en la misma sesión → ACEPTADO como Approved +Scope signal: M (revisión); L del sistema +Specialists: game-designer, systems-designer, ai-programmer, qa-lead, level-designer, godot-specialist, ux-designer, creative-director (síntesis senior) +Blocking items: 4 (consolidados de 7 findings bloqueantes de especialistas) | Recommended: ~11 | Prior verdict resolved: Sí (los 6 del round 2 se sostienen; estos son nuevos) + +**Resumen (síntesis del creative-director)**: El doc mejoró, pero el meta-patrón del round 2 recurrió — gaps *computables* dejados como prosa de disciplina de autor en lugar de cerrarse con el loud-fail que el doc exige en todas partes — y, peor, el *mecanismo* de loud-fail estaba roto. "Una loud-fail doctrine que compila a silencio en release es peor que ninguna: fabrica falsa confianza." Bloqueante de coherencia, no de pulido. Todos cerrables en sesión salvo la pregunta de identidad de diseño (Furia vs. fantasía), que el usuario resolvió reconciliando la prosa. + +**4 bloqueantes (consolidados) — resueltos en la misma sesión:** +1. [godot-specialist] `assert()` es no-op en builds exportados → toda la loud-fail doctrine no validaba en release. → Reemplazado doc-wide por `push_error()` + halt explícito (con ejemplo de código); centralizar carga de era. +2. [qa-lead + systems-designer] Patrón recurrente: (a) 6 de 10 loud-fail configs sin AC → AC-K79–K84; (b) acoplamiento F4×F2 solo-prosa mientras su gemelo F2×HP-héroe tenía AC-K74 → enforced como loud-fail cross-resource (AC-K85), corregido min→max troop DPS, con flag de excepción `allow_single_troop_neutralize`. +3. [systems-designer] Furia (b) degeneraba en N=2 con el default f=0.5 (`ceil(0.5×2)=1`, cebo único evadía) → fórmula a `floor(f×N)+1` (>N/2 en default), dominio estrechado `(0,1]`→`[0.34,1.0]`, AC-K63/b/c/d actualizados + AC-K86. +4. [ai-programmer] Cluster de determinismo: `entity_id` indefinido (si es `get_instance_id()` no es reproducible), cita errónea a AC-C06 (no tiene cláusula de empate), doble `apply_damage` en el frame de `STRIKING`, race de prioridad de victoria. → `entity_id` definido como contador de spawn estable; cita corregida; kaiju excluido del bucle de auto-ataque base (dueño único de `apply_damage`) + abort síncrono tras cruce de 0 (AC-K87/K88); RNG acotada a same-plataforma (AC-K71). + +**Decisiones del usuario (2026-08-12):** (a) F4×F2 → enforce loud-fail; (b) N=2 Furia → `>N/2` (floor(f×N)+1); (c) Furia esbirro-collateral + fantasía → exención de esbirros (cuenta daño a kaiju o esbirros, AC-K63e) + reencuadre de Player Fantasy (Furia = presión de tempo, no redirect oculto); (d) KA-1 accesibilidad → canal no-auditivo de tercio 1 (viñeta de borde + rumble, KV-1b/KV-10). + +**Desacuerdo adjudicado:** systems-designer marcó AC-K74 como posible bloqueante de computabilidad; verificado contra `datos-de-era-civilizacion.md` que `EraDefinition` anida `unit_roster` (porción de héroes) + `kaiju_definition` y bloquea hasta `LOADED` → es computable; degradado a un doc-gap de una línea (tabla Dependencies actualizada). godot-specialist concurrió. + +**Recomendados NO accionados (siguen advisory):** F5×F4 margen de neutralización casi nulo en los pisos de rango; baseline MVP de histéresis de KU-2; separabilidad perceptual KV-9/KV-10; reward hook de esbirros + solape de oleadas en steady-state; medición de `max_concurrent_esbirros`; mock de AC-K58 (forma escalar-vs-mapa). Candidatos a Open Questions / próximo `/ux-design` / playtest. + +**No accionados (pertenecen a otros docs):** OQ-10 (lock/trigger de stats del kaiju en `combate-dano.md`); des-flaggeo cross-GDD de notas "sin GDD" stale (OQ-4). + +**Estado tras la revisión:** GDD `Status: Approved` (2026-08-12). systems-index.md actualizado a **Approved** (#9); reviewed 3→4, approved 3→4. Aceptado por el usuario sin 4ª pasada de agentes. Recomendación técnica pendiente: `/architecture-decision` para el nodo dueño del ciclo de intención + orden de tick (el GDD ya fija los requisitos duros; el ADR los ejecuta). diff --git a/design/gdd/reviews/forja-de-legado-review-log.md b/design/gdd/reviews/forja-de-legado-review-log.md new file mode 100644 index 0000000000..538c38f89c --- /dev/null +++ b/design/gdd/reviews/forja-de-legado-review-log.md @@ -0,0 +1,38 @@ +# Review Log — Forja de Legado + +## Review — 2026-08-17 — Verdict: NEEDS REVISION → revisado y aceptado (Approved) +Scope signal: M +Specialists: game-designer, systems-designer, qa-lead, godot-specialist, creative-director (senior) +Blocking items: 2 | Recommended: 8 +Prior verdict resolved: First review + +**Veredicto del creative-director (síntesis):** documento fuerte, disciplinado y thin-by-design, honesto sobre sus deferrals. Dos ítems bloqueantes tocaban la única garantía que Forja existe para proteger (una muerte → exactamente una reliquia, nunca cero): la clave de dedup podía producir un cero-reliquia silencioso, y la contradicción con el piso de Héroes podía hacer loud-fail en cada muerte accidental. Ambos contract-level, no cosméticos. NEEDS REVISION, no MAJOR. + +### Bloqueantes (2) — aplicados +1. **[systems-designer] Clave de dedup insegura.** `(source_hero_id, origin_era_id)` podía tragar una 2ª reliquia legítima porque la unicidad de `hero_id` no estaba especificada en ningún GDD. **Fix:** Regla 8 declara el contrato de instancia única de héroe (no respawn; `DEAD` terminal, Héroes AC-H27b) + nuevo **AC-FL26** (loud-fail si un roster de era tiene `hero_id` duplicado). +2. **[systems-designer] Contradicción cross-GDD del piso.** `base_accidental` de Héroes permitía `0.05` (rango 0.05–0.15); en `D=0` la salida H4 ES `base_accidental`, cayendo bajo el dominio `[0.10,1.00]` de Forja → loud-fail en cada muerte accidental. **Fix (upstream):** rango de `base_accidental` estrechado a **0.10–0.15** en `sistema-de-heroes.md` (2 lugares) + registry `entities.yaml`; nota de reconciliación en Forja Regla 5. + +### Recomendados (8) — aplicados +- R1: FL1 gana tolerancia `EPSILON_TIER = 1e-6` para comparar boundaries de floats computados (riesgo real en 0.70, alcanzable vía T≈0.857). +- R2: `relic_id` declarado `String`, único global al legado, estable (Regla 4 + OQ-FL5). +- R3: OQ-FL4 gana 4 ítems de scope obligatorio (mismo-frame/sin-deferred para AC-FL15; `assert()`+`push_error()` activo en release; serialización int64; bloqueo gateado por `await`). +- R4: símbolo de entrada `receive_relic` nombrado para los greps de CI (AC-FL02/FL12/FL18); ruta de módulo diferida a OQ-FL4. +- R5: AC-FL13 reescrito para aseverar membresía en registro (M→M+1), no forma del record (era redundante con FL03/FL08/FL01). +- R6: AC-FL09 inline del producto 3×3 categoría×tier. +- R7: AC-FL24 nombra el seam de inyección (`ActiveEraProvider` stub). +- R8: nota de serialización de `forge_counter` (int64 vía `store_var`/`get_var`, no JSON) en Edge Cases + ADR. + +### Corregido en el review mismo +- **"Tier Sólida inalcanzable"** (systems-designer, repetido por el CD): **incorrecto**. Conjunto real de `relic_quality` con constantes MVP = `{0.10} ∪ [0.40, 1.00]`; Sólida `[0.35,0.70)` es alcanzable desde `0.40`. Solo la franja `[0.35,0.40)` no se produce. Documentado como nota informativa en FL1, no bloqueante. + +### Disagreements surfaced (adjudicados) +1. **Anti-nihilismo socava stakes** (game-designer vs. CD + pilar lockeado): el CD dictaminó que el pilar se sostiene (stakes viven en tier/héroe nombrado, no existencia-vs-nada); no se revisita. Confirmado por el usuario. +2. **Ownership de "0.35 inalcanzable"** (systems-designer lo enmarcó como Forja; CD lo ruleó como señal de tuning de Héroes): resuelto como corrección numérica (ver arriba) — no era ni de Forja ni realmente un problema. + +### Registrado como riesgo/nota (sin cambio de reglas) +- **Pilar 2 diferido** (game-designer): riesgo de programa top-line en el header — el payoff jugable de Forja depende 100% de #12/#14 (sin GDD). +- **Epitaph** (game-designer): documentado el modelo — identidad narrativa única = `hero_name + origin_era_id + relic_category`; `epitaph` es etiqueta terse por diseño. + +### Housekeeping +- **OQ-FL1 resuelto**: Forja añadida a Dependientes de Datos de Era; fila de systems-index ya completa. +- Nota de Player Fantasy de modo lean actualizada (creative-director consultado en este review). diff --git a/design/gdd/reviews/input-review-log.md b/design/gdd/reviews/input-review-log.md new file mode 100644 index 0000000000..6bbb64bf53 --- /dev/null +++ b/design/gdd/reviews/input-review-log.md @@ -0,0 +1,26 @@ +# Review Log — Sistema de Input (`design/gdd/input.md`) + +Revision history for `/design-review` runs on the Input GDD. Newest entries first. + +## Review — 2026-08-19 — Verdict: NEEDS REVISION (blockers applied same session; pending re-review) +Scope signal: L (bordering XL — Foundation doc, 5 formulas, 5+ downstream dependents, likely needs 1 ADR + a camera-zoom-ownership decision) +Specialists: game-designer, systems-designer, qa-lead, performance-analyst, godot-specialist, ux-designer + creative-director (senior synthesis) +Blocking items: 7 | Recommended: ~12 (several folded in same session) + +Summary: First formal review. Architecture judged sound (abstraction layer, single-active-context state machine, intention-only signals) but not implementable-as-written: a literal implementation would (a) fire an engine pause during DEATH_HOLD, violating permadeath AC-P07c; (b) build a zoom system failing its own AC 13/AC 24; (c) chase an unmeasurable perf gate (AC 22). Creative-director's through-line: the doc claims Pilar 1 but contradicted it at the exact moment Pilar 1 matters most (a hero's death). Key adjudication: KEEP the DEATH_HOLD input discard (replaying a pre-death order violates the fiction) — the *silence* was the defect, not the discard. Pilar 1 guarantee reframed from an absolute single-doc claim to a distributed cross-system invariant with named owners. + +7 blockers resolved this session: +1. Gamepad-disconnect × DEATH_HOLD — carved exception + AC 17b; named `joy_connection_changed` + GUID identity. +2. Zoom direction — locked scroll-up = zoom-in → scalar toward 0.5; split `±` into two expressions; fixed AC 13, added 13a/13b; Camera2D mapping handed to camera GDD (OQ-INPUT-2). +3. DEATH_HOLD lockout feedback — `input_lockout_active` + firm cursor-hide + art-bible 7.3 cross-ref; AC 15b. +4. Pilar 1 distributed invariant — Player Fantasy reworded; frame-order race → OQ-INPUT-1. +5. AC 22 — reclassified on-device Integration/Performance (`tests/performance/input/`), harness specified; frame-budget gap → OQ-INPUT-4. +6. Formulas — `stick_dead_zone` remap expression + `drag_threshold_px` DPI-scaling formula added; `drag_threshold_px` added to accessibility knobs. +7. Bidirectionality — Input registered in ui-hud.md, reliquias-bendiciones.md, and systems-index (enumeration + Dependency Map); stale provisional notes refreshed. + +Also folded in: `current_context` enforcement pattern (OQ-INPUT-5 ADR), central per-context gating, dual-focus scope correction, device-switch debounce (AC 5c), first-launch platform default (AC 5b), "acción en curso" generalization, classification of AC 19/20/21. + +Open contracts carried forward: OQ-INPUT-1 (Pilar 1 invariant / frame order — ADR candidate), OQ-INPUT-2 (Camera2D.zoom mapping), OQ-INPUT-3 (no-gamepad flow), OQ-INPUT-4 (frame-time budget), OQ-INPUT-5 (current_context ADR). + +Prior verdict resolved: First review. +Next: re-review in a clean session (`/clear` then `/design-review design/gdd/input.md`) to confirm the 7 fixes hold and the distributed-invariant reframing introduced no new cross-GDD issues. diff --git a/design/gdd/reviews/permadeath-review-log.md b/design/gdd/reviews/permadeath-review-log.md new file mode 100644 index 0000000000..bdf0a15fd4 --- /dev/null +++ b/design/gdd/reviews/permadeath-review-log.md @@ -0,0 +1,264 @@ +# Review Log — permadeath.md + +## Review — 2026-08-12 — Verdict: MAJOR REVISION NEEDED +Scope signal: L (trending XL — driven by a cross-document contract pass, not by Permadeath's own size) +Specialists: game-designer, systems-designer, qa-lead, godot-specialist, creative-director (senior synthesis) +Blocking items: 5 | Recommended: 5 | Nice-to-have: 3 +Prior verdict resolved: First review + +### Summary (creative-director synthesis) +The core loop design is sound and the pillars are well-served in intent, but the +document made a cluster of load-bearing claims about state, data, and ownership it +does not actually control — several provably false against the very Approved GDDs it +depends on. Three silently voided Pilar 2 ("every death yields a relic") or Pilar 1 +(death-as-tragedy) at runtime. Root cause: Permadeath is architected as a *thin +orchestration layer* yet repeatedly asserts authority over state it doesn't own +(Héroes' DYING/DEAD lifecycle), data it doesn't produce (relic_quality), and calls it +doesn't make (Guardado's autosave) — and those false claims live in already-Approved +GDDs, so fixing Permadeath in isolation only relocates the contradiction. + +### Blocking items (all 5 addressed in this revision) +1. **Payload/relic_quality contradiction** (systems-designer, qa-lead, game-designer — + 3 independent flags). Héroes AC-H12 exposes only D/T/P; Permadeath Rule 1/5 + AC-P02 + assumed relic_quality already in the payload. AC-P01 and AC-P16 Givens were mutually + unsatisfiable. → **Fix**: decided Héroes computes H4 at the DYING snapshot and includes + relic_quality in the payload. Updated Overview, Rule 1, AC-P01, AC-P02. Cross-GDD edit + to Héroes AC-H12 logged as OQ-P6. +2. **Crash during HOLDING → hero stuck in DYING with no relic** (systems-designer, + game-designer; CD elevated to #1 — save corruption, hardest to detect in production). + Autosave fires at DYING (before Permadeath gets payload), persisting state=DYING; + relic only produced at RESOLVED. AC-P18/Héroes AC-H27's "loads as DEAD" was false. → + **Fix**: new Rule 8b (load-time DYING→DEAD coercion) + Rule 6 now requires the DYING + autosave to persist the full payload incl. relic_quality. Rewrote crash Edge Case and + AC-P18. Cross-GDD edits to Héroes/Guardado logged as OQ-P6. +3. **Pause-mechanism ambiguity** (godot-specialist — fresh blocker none of the others + caught). GDD never said whether "pausa global" = real SceneTree.paused. If so, the + hero's resolution animation (feeding animation_done) freezes and EVERY death falls to + the 8s timeout, masking AC-P04's normal path; or consumers can't poll the flag. → + **Fix**: Rule 4 now mandates cooperative flag-polling, never SceneTree.paused; beat + node PROCESS_MODE_ALWAYS; hero animation must too. Edge Case corollary + new AC-P07b. +4. **AC-P17 circular/untestable** (qa-lead). "resolves once, doesn't reprocess" named no + observable. → **Fix**: rewrote — DEAD confirmation invoked exactly once AND Forja + receive_relic exactly once, even if release fires on multiple ticks. +5. **QUEUED battlefield visual unspecified** (game-designer — Pilar 1 gap). Doc never + said what a queued dying hero looks like (frozen mid-collapse vs. unwitnessed). → + **Fix**: Rule 3 + States table — hero holds at collapse threshold, full V-5→V-6 beat + plays only on entering HOLDING. New AC-P22. + +### Recommended (all addressed) +- Autosave-ownership three-way disagreement reconciled (Rule 6 + cross-GDD action list). +- Pilar 3 pacing safeguard added: Rule 8 total-lockout cap + 2 tuning knobs + (death_hold_total_lockout_cap_s, death_hold_min_compressed_s) + AC-P21. +- AC-P07 now asserts isolation (frozen consumer clock), not just beat progress. +- OQ-P4 downgraded from "settled" to open contract (Temporizador GDD only defines the + global menu pause, never game_time_paused); crash Edge Case softened accordingly. +- OQ-P5 gains an era-reset design sub-question (does the FIFO queue persist across eras?). + +### Nice-to-have (addressed) +- Section B framing tightened (the beat seals/reveals a decision already made at the + HP-zero snapshot; it is not the deciding instant). +- AC-P09/P11 recategorized Integration → Logic/unit. +- (Left open) Kaiju mid-STRIKING lands an unpaused hit today — already tracked in OQ-P1/AC-P14. + +### Outstanding after this revision +Blockers #1 and #2 are specified inside permadeath.md but **not closed at the system +boundary** — they require a cross-GDD contract pass (OQ-P6) re-opening the Approved GDDs +Héroes (AC-H12 payload, crash coercion), Guardado (autosave owner + payload persistence), +and Kaiju (game_time_paused consumption). Likely via /propagate-design-change, coordinated +with the producer. Next action chosen by user: re-review in a fresh session (/clear first). + +--- + +## Review — 2026-08-14 — Verdict: NEEDS REVISION (re-review, then revised same session) +Scope signal: L (fixes are a focused patch, but carry cross-GDD contract growth + a new ADR-adjacent decision) +Specialists: game-designer, systems-designer, qa-lead, godot-specialist, creative-director (senior synthesis) +Blocking items: 4 (new) | Recommended: 5 | Nice-to-have: 1 +Prior verdict resolved: **Yes** — all 5 prior blockers (2026-08-12) confirmed genuinely addressed. The 4 new blockers were introduced/left-open by that revision, not carryovers. + +### Summary (creative-director synthesis) +The prior payload/crash/pause-mechanism/serialization fixes hold with real rigor — a +strong document. But the revision introduced a tight cluster of NEW blockers, three +touching the pillars the system serves. Not a rewrite; a focused patch. Strongest signal: +Rule 8 compression was hit from three independent angles (math, determinism, dramatic +weight) — highest-priority fix. + +### New blocking items (all 4 addressed in this same-session revision) +1. **Pause-menu × DEATH_HOLD** [godot-specialist]. AC-P07b only proved SceneTree.paused + stays false because Permadeath never sets it — said nothing about Guardado's global pause + menu (Temporizador AC-T22) firing mid-beat, which could resolve DEAD off-screen. → **Fix**: + Rule 4 suppresses the pause action during HOLDING (no real engine pause can start); new + AC-P07c; PU-2/Input updated; hero-animation PROCESS_MODE_ALWAYS requirement hoisted into + OQ-P6 (was only in an Edge Case). User decision: suppress the menu (beat uninterruptible, + Pillar 1; only 2-8s). +2. **Formula P1 no minimum margin** [systems-designer]. Legal config (min=4.0, timeout=4.0001) + gives a sub-frame animation window → every death silently falls to timeout, masking AC-P04. + → **Fix**: constraint now timeout ≥ min + min_animation_window; new knob + death_hold_min_animation_window_s (0.5); AC-P08 rewritten to test the margin. +3. **Rule 8 compression broken** [systems-designer ×2 + game-designer]. Cap provably violated + (4.0+1.2+1.2=6.4 > 6.0), non-deterministic ("se acerca" unquantified), hero_id duration + favoritism, AND compression hit the LAST/heaviest death (inverting Pillar 1 at its peak). + → **Fix**: uniform compression — all beats use clamp(cap/N, min_compressed, min_duration), + cap met by construction, no favoritism (chosen over "protect last death" to preserve PU-3 + anti-editorializing). Load-time feasibility loud-fail (AC-P08b). AC-P21 rewritten with + regression case; new AC-P21b. +4. **AC-P18 not ownable** [qa-lead]. Coercion runs entirely in Héroes+Guardado. → **Fix**: + narrowed AC-P18 to Permadeath scope (empty queue, no resume); split coercion into AC-P18-ext, + relocated to Héroes/Guardado via OQ-P6. + +### Recommended (all addressed) +- Force V-6 relic reveal under timeout path (Rule 2 + PV-5 + AC-P06b) [game-designer]. +- Signal-push hybrid (game_time_paused_changed) + 4.6 editor spike before OQ-P5 ADR [godot]. +- Drop AC-P17's untestable "más en general" clause; add AC-P17b (dequeue-before-invoke no-op); + trim AC-P07b restatement/unobserved assertion [qa-lead]. +- Design note on the no-accelerate-on-retry tradeoff [game-designer]. + +### Disagreement adjudicated +- Witness "tell" (tremor/glow on QUEUED hero) [game-designer] vs PU-3 understatement. User + chose NO tell — kept understatement; recorded as a conscious decision in Rule 3. + +### Nice-to-have (addressed) +- Combate/Kaiju process_mode flagged as open risk in OQ-P1 rather than assumed [godot]. + +### Outstanding after this revision +OQ-P6 cross-GDD reconciliation still open by design — B1/B4 + prior payload/crash blockers +require re-opening Approved Héroes/Guardado/Kaiju GDDs (now 5 items incl. the animation +PROCESS_MODE_ALWAYS hoist and AC-P18-ext relocation). Likely /propagate-design-change with +the producer. Next action chosen by user: re-review in a fresh session (/clear first). + +--- + +## Review — 2026-08-14 #2 — Verdict: NEEDS REVISION (re-review, then revised same session) +Scope signal: L (focused patch, but carries cross-GDD contract growth — Datos de Era now a 6th OQ-P6 target) +Specialists: game-designer, systems-designer, qa-lead, godot-specialist, creative-director (senior synthesis) +Blocking items: 3 (new) | Recommended: 4 | Nice-to-have: 1 +Prior verdict resolved: **Yes** — all 9 prior blockers (2026-08-12 ×5, 2026-08-14 ×4) confirmed genuinely addressed. The 3 new blockers were introduced/left-open by the prior revision, not carryovers. + +### Summary (creative-director synthesis) +Core loop and both prior rounds' fixes hold — a strong document, not a rewrite; a focused cluster. +Highest priority is a regression of last round's OWN fix: uniform compression closed the cap +violation only under simultaneous arrival, not staggered arrival. The N_max read (hit independently +by 3 specialists + main review) is mostly cheap reconciliation but surfaces facets OQ-P6 didn't track. + +### New blocking items (all 3 addressed in this same-session revision) +1. **Rule 8 cap still exceeded under staggered arrival** [systems-designer, CD-confirmed]. With DEFAULT + knobs (min=2.0, cap=9.0, min_compressed=1.2) + N_max=5, staggered arrivals give 2.0+4×1.8=9.2 > cap. + The "muerte tardía recomputa N" clause (line 52) was the bug. → **Fix**: since Rule 4's game_time_paused + freezes all game-time damage during HOLDING, no death can join a chain mid-beat — so **N is fixed at + chain formation** (same-frame multi-kill). Removed the recompute clause; uniform cap/N over fixed N holds + the cap exactly (load-check now provably sufficient, per-era). Defensive "new chain" path for contract + violations. New AC-P21c (the exact 9.2 regression config). Preserves round 2's uniform/no-favoritism decision. + Ties the cap guarantee explicitly to OQ-P1 (Kaiju must honor game_time_paused — no longer cosmetic). +2. **N_max unsourceable + bidirectional-dependency violation** [systems-designer + qa-lead + godot-specialist, + 3-way convergence]. EraDefinition schema (Datos de Era AC-1) has no hero-roster-count field; the dependency + is absent from both GDDs' tables and systems-index.md; load-order race vs LOADING→LOADED; per-era vs global + cardinality; AC-P08b mislabeled [Logic/unit] non-blocked. → **Fix**: declared Datos de Era dependency in + Permadeath's upstream table (⚠️ provisional); cardinality = **per-era, read on era-LOADED signal** (not + _ready() — closes race); AC-P08b rewritten to inject N_max as a parameter (unit-testable now); new AC-P08c + (blocked) for real wiring; cross-GDD action #6 + OQ-P6 (field + reverse dependency + systems-index row). + User decision: per-era, re-validate on LOAD. +3. **PV-2 "art breathes" guarantee silently voids under compression** [game-designer]. → **Fix**: PV-2 now + states the guarantee is relative to the *effective* floor; under compression it drops to min_compressed + by design (Pilar 3 wins the rare worst case). User decision: keep the cap (compress), metronome risk logged + as an explicit playtest metric in Rule 8. + +### Recommended (all addressed) +- Metronome differentiation → playtest watch-metric in Rule 8 (can player still name all 3 fallen heroes?) [game-designer]. +- No-accelerate-on-retry habituation → concrete signal: suppressed-input attempts correlated with retry count [game-designer]. +- 8s silent timeout reads as a hang → non-editorializing "still-alive" cue candidate in timeout Edge Case [game-designer]. +- OQ-P5 era-reset → explicit Permadeath.reset_queue() (no passive Godot 4.6 "era changed" hook); fold into 4.6 spike [godot-specialist]. + +### Disagreement adjudicated +- game-designer's B2 remedy ("legibility floor, let chain exceed cap") vs Rule 8's Pilar 3 pacing purpose. + Real Pilar 1 vs Pilar 3 tension. User chose KEEP THE CAP (compress); concern kept as playtest metric, remedy downgraded. + +### Nice-to-have (addressed) +- Explicit N ≥ 1 assertion added to Rule 8 (guards division-by-zero) [systems-designer]. + +### Outstanding after this revision +OQ-P6 cross-GDD reconciliation still open by design — now **6 items** (added: Datos de Era hero-roster-count +field + reverse dependency + systems-index row). Requires re-opening Approved/Designed Héroes/Guardado/Kaiju/ +Datos de Era GDDs. Likely /propagate-design-change with the producer. Next action chosen by user: re-review +in a fresh session (/clear first). + +--- + +## Review — 2026-08-15 (4th round) — Verdict: NEEDS REVISION (re-review, then revised same session) +Scope signal: M (doc patch) / L (the cross-GDD propagation the doc patch could not close) +Specialists: game-designer, systems-designer, qa-lead, godot-specialist, creative-director (senior synthesis) +Blocking items: 5 (new) | Recommended: 6 | Nice-to-have: 0 +Prior verdict resolved: **Yes** — all 12 prior blockers (2026-08-12 ×5, 2026-08-14 ×4, 2026-08-14 #2 ×3) +confirmed genuinely addressed. The 5 new blockers were introduced/left-open by that revision, not carryovers. + +### Summary (creative-director synthesis) +Prior rounds' fixes hold. Root cause repeats in a 4th place: Rule 8's total-lockout cap only bounds the +*floors* of a compressed chain, not a beat that falls to the animation-bug timeout path — same failure mode +(asserting a guarantee over wall-clock time Permadeath doesn't fully control) as the payload/crash/pause +issues from rounds 1-3. Two specialists (game-designer, systems-designer) found this independently via +different methods. Verdict: fix what Permadeath owns, then stop re-reviewing this document in isolation — +the remaining risk lives in 4 sibling GDDs (OQ-P6). + +### New blocking items (4 addressed in this same-session revision; OQ-P6 explicitly left for propagation) +1. **Rule 8 cap ignores the timeout path** [game-designer + systems-designer, independent convergence]. A + compressed chain where even one beat falls to the 8s animation-bug timeout can blow the cap 1.7x-4.4x. + User chose: scope the cap's guarantee to the bug-free path (no formula changes — tightening the timeout + under compression would reintroduce the sub-frame-window risk AC-P08 was hardened against). New "Alcance + de la garantía del cap" paragraph in Rule 8 + caveat on AC-P21. +2. **AC-P01/AC-P02/AC-P06b testability gap** [qa-lead]. AC-P01 asserted a payload contract (`relic_quality`) + that Héroes' real AC-H12 contradicted, without the `(bloqueado)` marker other blocked ACs carry. → Fix: + AC-P01/AC-P06b marked `(bloqueado)` + Mock Contract Assumption notes, moved to Integration (bloqueado) + gate row; AC-P02 kept Logic/unit (genuinely fixture-testable) with its own assumption note. +3. **`reset_queue()` softlock gap** [game-designer, elevated by creative-director]. Only fires on era-LOADED; + a mid-era retry/reload could leave `HOLDING`/`QUEUED` state with input permanently suppressed. → Fix: + OQ-P5 invariant broadened to "empty at every point the game resumes control from a load," not just eras. +4. **OQ-P5 engine spike only gated the ADR, not Approved** [godot-specialist], despite Rules/ACs already + treating its assumptions as settled fact (PROCESS_MODE_ALWAYS + game_time_paused). → Fix: spike elevated + to a hard gate before Approved; scope expanded to cover `Engine.time_scale` interaction. +5. **OQ-P6 cross-GDD reconciliation — still fully unactioned after 3 prior rounds** [qa-lead + creative-director, + independently re-verified against the actual sibling GDD files, not the doc's own claims]. Not fixable by + editing permadeath.md alone. → Left explicitly for a follow-up propagation pass (see below). + +### Recommended (all addressed) +Float-tolerance caveat on AC-P21's cap claim; MVP defaults only tolerate N_max≤7 (sanity-check flagged); +camera framing during QUEUED/HOLDING flagged as unspecified (cross-GDD candidate to Héroes); PROCESS_MODE_ALWAYS +vs. Engine.time_scale conflation flagged; PU-2 mouse-focus path under 4.6 dual-focus routing flagged; habituation +playtest metric flagged as ownerless (needs an Input logging path that doesn't exist). + +### Outstanding after this revision +OQ-P6 (6 items) not closable inside permadeath.md — resolved in a follow-up session the same day, see below. + +--- + +## Cross-GDD Reconciliation — 2026-08-16 (OQ-P6, all 6 items) + +`/propagate-design-change` was invoked but didn't fit: `design/gdd/` has no git history (untracked) to diff, +and `docs/architecture/` has no ADRs yet — both of that skill's preconditions were absent. Resolved instead by +editing the 4 sibling GDDs directly, one at a time, each shown as a diff and approved before writing (same +draft → approval → write protocol as everywhere else in this project). + +1. **Sistema de Héroes** (`design/gdd/sistema-de-heroes.md`): AC-H12 now exposes `relic_quality` (evaluated via + H4 at the snapshot frame); AC-H26 now requires the full death payload marked "pending seal"; new AC-H27c + implements the DYING→DEAD crash-coercion + relic-forging contract (AC-P18-ext lives here, not in Permadeath); + V-5/V-6 section documents the `PROCESS_MODE_ALWAYS` requirement for the resolution-animation node and its + particle/audio/tween children. Does not reopen the Approved verdict (additive, no prior decision contradicted). +2. **Guardado/Persistencia** (`design/gdd/guardado-persistencia.md`): fixed the same autosave-trigger + misattribution in 6 places (Rule 1, Interactions table, provisional-GDD note, a design note, Dependencies + table, AC-01/AC-01b) — Sistema de Héroes triggers the autosave at `DYING` (AC-H26), never Permadeath. +3. **Encuentro con Kaiju** (`design/gdd/encuentro-con-kaiju.md`): new Core Rule 11 — the kaiju's entire intention + cycle (SEEKING/TELEGRAPHING/STRIKING/STAGGERED, all its internal clocks) freezes cooperatively while + `game_time_paused=true`, read via polling, never `SceneTree.paused`. New AC-K89. Closes OQ-P1. +4. **Datos de Era/Civilización** (`design/gdd/datos-de-era-civilizacion.md`): `EraDefinition` gains + `get_hero_roster_count()` — a derived accessor computed from `unit_roster`'s hero portion at `LOADED` time, + not a new authored field (avoids a second source of truth that could desync from the real roster). New + AC-1b. Permadeath added to both the Interactions and Dependencies tables. + +Permadeath itself was then updated to close the loop: OQ-P1 and OQ-P6 marked ✅ resolved; AC-P01, AC-P08c, and +AC-P14 reclassified from `(bloqueado)` back to normal Integration (contract confirmed, pending only +implementation, not design) — moved out of the "Integration (bloqueado)" gate row, which now holds only +AC-P06b (blocked on OQ-P2, unrelated) and AC-P16 (blocked on Forja de Legado, which still has no GDD). +`systems-index.md`'s Permadeath row updated to match. + +### Outstanding +Not yet Approved. Remaining before that gate: the OQ-P5 engine spike (now a hard gate, not yet run), OQ-P2 +(exact `animation_done`/force-reveal signal shape), OQ-P3/AC-P16 (Forja de Legado has no GDD yet), and a +formal re-review in a clean session to confirm the reconciliation pass didn't introduce anything new. Next +action: user's call — likely `/clear` then `/design-review design/gdd/permadeath.md` once ready. diff --git a/design/gdd/reviews/reliquias-bendiciones-review-log.md b/design/gdd/reviews/reliquias-bendiciones-review-log.md new file mode 100644 index 0000000000..221ca3afe0 --- /dev/null +++ b/design/gdd/reviews/reliquias-bendiciones-review-log.md @@ -0,0 +1,32 @@ +# Review Log — Reliquias/Bendiciones (#12) + +Historial de revisiones de `design/gdd/reliquias-bendiciones.md`. Cada entrada registra veredicto, alcance y hallazgos clave para que las re-reviews puedan rastrear qué cambió. + +--- + +## Review — 2026-08-18 — Verdict: NEEDS REVISION +Scope signal: L +Specialists: game-designer, systems-designer, economy-designer, qa-lead, ux-designer, audio-director + creative-director (síntesis senior) +Blocking items: 7 | Recommended: 9 | Nice-to-have: 4 +Prior verdict resolved: First review + +**Resumen (síntesis creative-director):** El andamiaje mecánico es sólido —disciplina de fuente-de-verdad, RNG determinista, invariantes loud-fail, tick math correcto, desbloquea 4 contratos downstream— pero la entrega de la Player Fantasy (la razón de ser del sistema, un eje del Pilar 2) no se cumplía en la capa mecánica. Tres bloqueantes elevados a co-blocking por el senior: (1) stacking sin cap → burst 25→79; (2) drafteo intra-era que contradecía el tono trágico y el onboarding del concepto; (3) gap de entrega de fantasía (9 combos categoría×tier + epitafio de plantilla = "drop genérico con nombre", agravado por el draft en tiempo real ilegible bajo presión de kaiju). Convergencia más fuerte: **stacking sin cap**, hallado independientemente por game-designer, economy-designer y systems-designer. El senior bajó 2 hallazgos: pool_cap como "dead config MVP" (es higiene forward-scoped, no defecto) y colisión de vocabulario coral de audio (arbitraje de mezcla en producción, no gate de diseño). + +**7 bloqueantes (todos aplicados el 2026-08-18 en la misma sesión):** +1. Stacking sin cap → **F-RB1 refresh-on-repick por `(relic_id, unit)`** + cota worst-case computable (AC-RB31/RB31b, AC-RB27 re-keyeado). +2. Categorías no cross-ancladas → reglas de **anclaje cross-categoría** en Formulas + validación de dominancia en OQ-RB8(d). +3. Fantasía no entregada → **identidad glanceable** (retrato/silueta + nombre) como canal primario (RV-2/RU-3); nota de individuación honesta; **OQ-RB10** enruta individuación más profunda a Forja. +4. Drafteo intra-era → **Regla 10** (pool solo de eras previas, nunca same-fight; MVP pre-siembra un panteón de prueba); **OQ-RB5 resuelto** (política fijada). +5. OFFERING vs. control de unidades sin especificar → **Regla 7b cámara lenta** (`offering_time_scale` 0.2×, unidades vivas, sin emboscada); commit-on-choice + soft-timeout + cooldown; 3 knobs nuevos; cascada a Regla 6, tabla de estados, AC-RB15/15b. +6. AC-RB13 dependencia oculta del hook no confirmado de Combate → acotado a `effective_stat`; integración diferida a AC-RB32 (ya bloqueado). +7. AC-RB22 sin barra estadística → concreto: N=1000 sorteos, semillas fijas [1..1000], 0 violaciones. + +**Fixes de soporte también aplicados:** RU-2b (`first_invocation_available` onboarding), RU-3b (`band_headroom` para evitar el trap de rally-whiff sin aviso). Registry (`effective_attack_damage`) re-sincronizado con la nueva regla de stacking. + +**Recomendados NO aplicados (para re-review/backlog):** AC-RB38b (race de retiro mid-OFFERING), AC-RB40b (relleno parcial de rally), invariante F-RB2 con paso mínimo perceptible (no solo `>`), F-RB5 accounting de reliquias withdrawn-for-offer vs. pool_cap, split AC-RB18/RB23, arbitraje de ducking de audio vs. telegrafío (OQ), motivo de audio por tier. + +**Decisiones de diseño del usuario (AskUserQuestion 2026-08-18):** pre-seed sin same-fight relics; one-instance-per-relic_id refresh; slow-mo durante OFFERING; anclaje cross-categoría + defer rally. + +**Nuevas Open Questions:** OQ-RB9 (propiedad del mecanismo de escala de tiempo global durante OFFERING), OQ-RB10 (individuación de reliquias más allá de categoría×tier, coordinación con Forja). + +**Estado:** revisión aplicada y **aceptada por el usuario como Approved el 2026-08-18 sin re-review adversarial** (decisión explícita del usuario: "marcalo como approved, debemos avanzar"). Salvedad registrada: 3 de los 7 fixes tocaron diseño no trivial (modelo de stacking refresh-on-repick, cascada de charge-timing por la cámara lenta, anclaje cross-categoría) y no fueron verificados por una re-review de especialistas. Los recomendados no aplicados (AC-RB38b, AC-RB40b, invariante F-RB2 con paso mínimo, F-RB5 accounting withdrawn-for-offer, split AC-RB18/RB23, arbitraje de audio) y la detección de regresiones quedan diferidos a `/consistency-check` + `/review-all-gdds` antes de arquitectura. diff --git a/design/gdd/reviews/sistema-de-heroes-review-log.md b/design/gdd/reviews/sistema-de-heroes-review-log.md new file mode 100644 index 0000000000..8f46e1444d --- /dev/null +++ b/design/gdd/reviews/sistema-de-heroes-review-log.md @@ -0,0 +1,101 @@ +# Review Log — Sistema de Héroes + +## Confirmación (lean) — 2026-08-07 — Verdict: APPROVED +Scope signal: L +Specialists: ninguno (confirmación single-session tras aplicar los fixes de Review 2) +Blocking items: 0 | Recommended: 0 (los diferidos siguen en backlog de vertical slice) +Summary: Pase de confirmación ligero sobre las secciones tocadas por los 2 bloqueantes de Review 2 (H3, H4, Enforcement, familia AC-H23, nuevas ACs). Verificado: arithmética de AC-H23b (0.70) y AC-H23d (0.35) correcta; floors individuales y piso de suma ahora coinciden en 0.70 (consistente); política loud-fail uniforme reflejada en AC-H23 y Enforcement. El pase detectó y corrigió una contradicción residual (el ejemplo de suma mínima en "Interacción crítica" seguía citando 0.65 con el rango viejo de base_deliberate). Doc internamente consistente. +Prior verdict resolved: Sí — Review 2 (2 bloqueantes) cerrado y confirmado. Marcado Approved en systems-index.md. + +--- + +## Review 2 (re-review) — 2026-08-07 — Verdict: NEEDS REVISION → fixes aplicados +Scope signal: L (por contratos provisionales downstream, no por el tamaño de los fixes) +Specialists: systems-designer, game-designer, qa-lead, ux-designer, audio-director, narrative-director + creative-director (synthesis) +Blocking items: 2 (ambos NUEVOS, surgidos de la revisión previa) | Recommended: ~6 | Nice-to-have: ~6 +Prior verdict resolved: Sí — los 4 bloqueantes de Review 1 se confirmaron cerrados; la revisión introdujo 2 defectos nuevos. + +### Hallazgos y resolución +**BLOCKING (2/2 resueltos en esta sesión):** +- **B1 [systems-designer] — colisión T=0,P=0 en H4.** El enforcement de suma [0.70,1.00] protegía solo la reachability de Tier 3, no el extremo T=0,P=0 (donde relic_quality = base_deliberate exacto). Config legal (base_deliberate=0.30, sum=0.70) mandaba una muerte DELIBERADA a Tier 1, idéntica a una accidental. **Fix:** floor individual enforced `base_deliberate ≥ 0.35` (rango 0.30→0.35); Enforcement, Interacción crítica, tabla H4 y Tuning Knobs reescritas; AC-H23d añadida; entities.yaml actualizado. +- **B2 [systems-designer] — contradicción interna en H3.** Línea de fórmula decía "restringido a ≥0.5" (clamp) pero la fila de tuning decía "<48 → radio <0.5: pierde el piso". **Fix:** resuelto a validación de entrada (`sprite_diameter_px ≥ 48` loud-fail), no clamp de salida; garantiza ≥0.5 coherente con AC-H05. + +**Decisión del usuario (desbloqueó B1):** política de enforcement = **loud-fail uniforme** (todos los knobs fuera de rango fallan ruidosamente; se elimina el clamp individual silencioso de AC-H23, coherente con AC-H02). AC-H23 reescrita clamp→loud-fail; AC-H23b reescrita (los floors individuales ahora suman exactamente 0.70, la suma-check queda como defensa-en-profundidad); AC-H23c limpiada; cláusula de aislamiento de test añadida a Enforcement (qa-lead). + +**RECOMMENDED aplicados:** +- A-4 [audio-director]: pin de outcome-blindness (cue idéntico en ambas direcciones + sin sting en el cruce del D-gate, que es casi-determinista); AC-H34 añadida (nueva sección G de audio). A-3: restricción negativa (identity-variation no debe correlacionar magnitud con la distribución de tier del arquetipo). +- (bloqueado)-tags [qa-lead]: AC-H30 y AC-H32b ahora taggeadas + Mock Contract Assumptions. +- AC-H32e [qa-lead/ux-designer]: nueva AC de daltonismo/grayscale (cubre la "regla permanente" de U-1 que AC-H32a no verificaba). +- D-gate [game-designer + narrative-director, convergentes]: OQ-11 registra la alternativa (ampliar D=1 a Hold sostenido contra probabilidad letal) + AC-H31b liga el playtest de vertical slice al estado de falla "culpa pura". NO se rediseña el confirm-step para el MVP (ruling del creative-director: es la solución anti-exploit correcta; el feel se valida con controlador, no con otra ronda en papel). +- A-5 [audio-director]: riesgo de telegrafío tragado documentado + OQ-10. +- Label [game-designer]: Combate/Comando → Combate/Daño en todo el doc + entities.yaml. + +**DIFERIDOS (recommended/nice-to-have, no aplicados — a vertical slice):** +- ACs completas para A-1/A-2/A-3/A-5 (solo A-4 tiene trigger discreto testeable hoy). +- AC-H31 rúbrica: tie-break para señales mixtas, unidad de análisis, ejemplos anclados, inter-rater check [qa-lead]. +- Retitular "Legendaria" (carga de rareza ARPG) [ux/narrative]; legibilidad del marco graduado de 3 estados a 24-32px LOD [ux]; regla de redondeo de H1 int [systems]; redundancia de esquema AC-H01/H01b [systems]. +- Riesgo estructural de loot-priming (la escalera de 3 tiers en sí, no su render) — hipótesis de playtest [narrative/ux]. + +**Convergencia clave:** game-designer + narrative-director independientemente coincidieron en que el confirm-step del D-gate sobre-corrigió a "ritual-as-checkbox" / mecánicamente idéntico entre héroes. Adjudicado por creative-director como RECOMMENDED (no blocking) para MVP → registrado en OQ-11 + AC-H31b, se valida en playtest. + +**Estado:** 2 bloqueantes + recomendados aplicados. Pendiente: confirmación final (idealmente re-review ligero tras /clear). Si confirma → Approved. + +--- + +## Revisión aplicada — 2026-08-07 — todos los bloqueantes + recomendados + housekeeping +Decisiones del usuario/autónomas: D-gate opción (c) combinada (comando Sacrificio con confirm-step + orden-activa + muerte-dentro-de-radio); tuning knobs sacrifice_radius=3.0, guard_radius=4.0; identidad de diseño = fórmula limpia por-héroe (escuela Hades), tragedia del contexto autoral; homogeneización resuelta con vía dual de P (defensiva u ofensiva). + +**BLOCKING (4/4 resueltos):** +1. H4 suma conjunta → enforcement loud-fail de base_deliberate+w_stakes+w_purpose ∈ [0.70,1.00] (Tuning Knobs "Enforcement" + "Interacción crítica" reescritas); AC-H23b/AC-H23c añadidas; nota falsa del registro corregida (entities.yaml, revised 2026-08-07). +2. D-gate → definición de D reescrita (3 condiciones) en H4; Regla 5, Edge Cases (5 casos incl. knockback-dentro-de-radio y confirm-step), AC-H21/H21b/H21c reescritas; sacrifice_radius/guard_radius como knobs; OQ-2 actualizada; snapshot de D clarificado. +3. UI daltonismo → U-1/U-2 reescritas a patrón multi-canal (forma/marco + movimiento + color terciario, art bible 7.5), resuelve también loot-priming; A-4 cue de audio/haptic en cruce de tier. +4. AC-H32 → dividida en AC-H32a-d + AC-H33 (marcador de selección U-6). + +**RECOMMENDED (aplicados):** +5. U-3 → trigger por time_to_death (contrato conjunto con Encuentro con Kaiju), no HP; OQ-6 actualizada; AC-H29 con Mock Contract Assumptions. +6. Homogeneización → vía dual de P; nota de simetría en H4; OQ-7 (alternativa pesos por arquetipo). +7. QA hardening → AC-H21 taggeada (bloqueado); "Mock Contract Assumptions" en AC-H11/H21/H25/H29; AC-H27b (DEAD inaccionable); AC-H31 con rúbrica de 3 registros + dueño qa-tester. +8. Audio → A-2 (silencio permitido, sin presuponer orquesta), A-3 (outcome-blind pero identity-varied), A-4 (audio de ventana de decisión), A-5 (ducking kaiju). ++ Schema (systems #3): abilities: Array[AbilityDefinition] en Regla 1 + OQ-9; AC-H01b (validación de stats derivados al cargar). Knobs pan_speed_fraction/sprite_diameter_px añadidos a Tuning Knobs. + +**HOUSEKEEPING:** +- Header GDD → Status "In Design — revisado post design-review", Last Updated 2026-08-07. +- control-y-seleccion-de-unidades.md → filas de Sistema de Héroes y Tropas actualizadas de "Sin GDD" a "Diseñado"; notas provisionales corregidas. +- entities.yaml → variable P renombrada protected_objective → served_purpose (comentario). + +**Estado:** GDD sigue "Designed" (NO Approved) — pendiente RE-CORRER /design-review tras /clear. Si pasa → Approved. +OQ abiertas nuevas: OQ-7 (pesos por arquetipo), OQ-8 (cue intra-banda), OQ-9 (schema de AbilityDefinition). + +--- + + +## Review — 2026-08-07 — Verdict: NEEDS REVISION +Scope signal: L +Specialists: game-designer, systems-designer, qa-lead, ux-designer, audio-director, narrative-director + creative-director (synthesis) +Blocking items: 4 | Recommended: 4 | Nice-to-have: 5 + 1 open design-identity decision +Prior verdict resolved: First review + +### Summary +GDD excepcionalmente disciplinado (0 lenguaje no-testeable en las 32 AC), arquitectura sólida, concepto intacto — no requiere rediseño. Pero 4 bloqueantes, dos de ellos en el corazón mecánico: (1) defecto de suma conjunta en Fórmula H4 que hace Tier 3 Legendaria matemáticamente inalcanzable en el mínimo conjunto de los rangos "seguros"; (2) el D-gate ("coincide exactamente") es a la vez inimplementable (knockback/pathfinding) y explotable (Hold reflejo). Un tercero viola la regla permanente de daltonismo del art bible (4.5) en el único elemento de UI que carga todo el payoff en tiempo real. El cuarto (AC-H32) tergiversa la completitud del propio doc. Todos los fixes son quirúrgicos, no estructurales. + +### BLOCKING (4) +1. **H4 defecto de suma conjunta** [systems-designer/qa-lead/game-designer]. En D=1, relic_quality = base_deliberate + w_stakes·T + w_purpose·P (base_accidental se cancela). Los 3 knobs se clampan individualmente pero la SUMA nunca se valida. Mínimo conjunto (0.30+0.20+0.15=0.65) → Tier 3 Legendaria INALCANZABLE (rompe AC-H18). Máximo conjunto (1.20) → Legendaria a T=0. Fix: assert de carga rechazando base_deliberate+w_stakes+w_purpose ∉ [0.70, 1.00] (patrón loud-fail de AC-H02) + AC nueva junto a AC-H23. ADEMÁS: corregir la nota falsa en design/registry/entities.yaml (fórmula relic_quality) que dice "sum = 1.00 (techo)" como invariante garantizado — solo es cierto en defaults; propagará el error a GDDs downstream. +2. **D-gate inimplementable + explotable** [game-designer/narrative-director/systems-designer]. "Destino coincide exactamente con el lugar de muerte" (Regla 5 / Edge Case / AC-H21) no funciona en 2D continuo (knockback/pathfinding → sacrificios legítimos marcan D=0 por mala suerte, y el readout puede mentir todo el combate y saltar a Tier 1 en el frame de muerte); pero Hold-default-a-tile-actual permite farmear D=1 con cero intención. NI D ("coincide") NI P ("radio de guardia") tienen tolerancia numérica definida. REQUIERE DECISIÓN DEL USUARIO (ver Disagreement A abajo). Actualizar OQ-2. +3. **U-1..U-4 violan la regla de daltonismo del art bible 4.5** [ux-designer]. Señaliza estado solo por intensidad de dorado; 4.5 exige respaldo de forma/animación/posición y nombra Oro Reliquia vs Hueso/Ocre como el par de mayor riesgo. Fix: heredar el patrón diegético del art bible 7.5 (cambio discreto de forma/marco + cue de audio/haptic en transiciones de tier). BONUS: este mismo cambio arregla el riesgo de "loot-priming" de narrative-director #4 (el glow dorado por tier se lee como rareza de loot ARPG para esta audiencia). +4. **AC-H32 obsoleta** [qa-lead]. Todavía dice que UI Requirements está "[To be designed]" y se declara advisory, aunque U-1..U-7 ya están escritas. Dividir en AC-H32a-d (transición discreta en boundaries de tier / umbral de visibilidad / freeze-on-snapshot / sin texto editorializante) + AC nueva para U-6 (marcador de selección héroe vs tropa, hoy sin cobertura). + +### DISAGREEMENTS a adjudicar +- **A (crítico) — cómo arreglar el D-gate**: game-designer dice que es demasiado ESTRICTO → order-intent-persistence + radio generoso de destino. narrative-director dice que es demasiado EXPLOTABLE → un comando Sacrificio distinto con confirm-step/costo, no un booleano reflejo. Recomendación del creative-director: HACER AMBOS (componen) — un verbo Commit/Sacrificio deliberado (peso + anti-exploit) cuyo D=1 resuelve vía orden-activa + muerte-dentro-de-radio (implementable + anti-mala-suerte). Sirve al Pilar 4. El usuario decide la forma exacta (confirm-step / costo de recurso / verbo dedicado). +- **B (menor) — A-3 (sin audio diferenciado por tier)**: audio-director en desacuerdo parcial con el propio GDD: A-3 sobre-corrige (aplana toda muerte en audio idéntico) y confunde "outcome-blind" (mantener) con "identity-blind" (sin resolver). creative-director coincide: apuntar a outcome-blind pero identity-varied. + +### RECOMMENDED (4) +5. Ventana de decisión de ~30s (AC-H29) puede ser estructuralmente ilegible: el trigger de visibilidad de U-3 (HP) está desacoplado de time-to-death. Hacerlo contrato conjunto con Encuentro con Kaiju (time-to-death/incoming-damage). Además, la ventana no tiene NINGUNA spec de audio. +6. Homogeneización de arquetipos: con P=0, héroes ofensivos necesitan T≥0.857 para Legendaria vs defensivos que la alcanzan fácil con P=1 → meta de "siempre parkear héroes en objetivos". Considerar pesos relativos al arquetipo o un equivalente ofensivo de P. +7. Endurecimiento QA: taggear AC-H21 (bloqueado) + backfill list; líneas "Mock Contract Assumptions" en las AC bloqueadas como gate de DoD en las stories de Permadeath/Kaiju/Combate; AC-H27b (actuar sobre héroe DEAD se rechaza); nombrar qa-tester como dueño de evidencia de AC-H31. +8. Resolución de feedback intra-banda (U-2/U-3): la mayor parte de la influencia táctica cae DENTRO de una banda de tier → decisiones reales pueden no producir cambio visible. + +### NICE-TO-HAVE + housekeeping +Onboarding de la primera exposición al dorado (ux #4); comparación multi-héroe de un vistazo (ux #5); relic_category como decisión de caracterización (narrative #3); runway de apego con 2-3 héroes/era (narrative #5); ducking de vocalización del kaiju (audio #4). Stale: header "Status: In Design / Last Updated 2026-07-23"; control-y-seleccion-de-unidades.md todavía lista este sistema como "Sin GDD (contrato propuesto)". + +### DECISIÓN DE IDENTIDAD DE DISEÑO (abierta, no bloqueante) +narrative-director: la fórmula es un scorer genérico idéntico por héroe, justificado citando muertes hand-authored (Arthas/Cristina/Quirón). Posición del creative-director: mantener la fórmula limpia y aprendible para el MVP es legítimo (escuela Hades) — que la tragedia venga del contexto autoral, no de variación de fórmula por héroe — PERO registrar la decisión conscientemente. Decisión del usuario. diff --git a/design/gdd/reviews/temporizador-de-preparacion-ritual-review-log.md b/design/gdd/reviews/temporizador-de-preparacion-ritual-review-log.md new file mode 100644 index 0000000000..1ff8b75a1a --- /dev/null +++ b/design/gdd/reviews/temporizador-de-preparacion-ritual-review-log.md @@ -0,0 +1,38 @@ +# Review Log — Temporizador de Preparación/Ritual + +## Review — 2026-08-09 — Verdict: NEEDS REVISION → fixes aplicados → Approved (sin re-review) +Scope signal: M (single system, 4 formulas, 5 dependencies, no new ADR — pero el fix de dependencias tocó 2 GDDs adicionales) +Specialists: game-designer, systems-designer, qa-lead, ux-designer, audio-director + creative-director (synthesis) +Blocking items: 5 | Recommended: 9 | Nice-to-have: ~12 | Disagreements adjudicated: 2 +Prior verdict resolved: First review + +### Summary +Arquitectura núcleo sólida (reloj único monotónico con fases como umbrales) — sin defectos estructurales. Los 5 bloqueantes fueron higiene de contrato entre documentos y un requisito de audio no escrito, no un rediseño. Cuatro pares de hallazgos convergieron independientemente entre especialistas de distintas disciplinas (margen de `imminent_window_s`, gap del guideline <20%, gap de schema de `EraDefinition`, contradicción de VA-1) — señal de alta confianza que se usó para priorizar qué era bloqueante. + +### BLOCKING (5/5 resueltos en esta sesión) +1. **[systems-designer] Contradicción de save-schema.** Línea 58 decía que se persiste `time_remaining_to_climax_s`; Edge Cases/AC-T23 decían `elapsed_s`. **Fix:** línea 58 corregida a `elapsed_s`, cross-referenciada con `guardado-persistencia.md`. +2. **[systems-designer, qa-lead, creative-director] Contratos de dependencia sobrestimados (3 partes).** + - (a) `datos-de-era-civilizacion.md` nunca nombraba `approach_duration_s`/`imminent_window_s` en su AC-1 ni en su lista de campos. **Fix:** ambos campos añadidos a Core Rule 1, AC-1, tablas de Interactions/Dependencies y la Open Question relevante de ese GDD. + - (b) `guardado-persistencia.md` nunca mencionaba este sistema — violaba la regla de bidireccionalidad del proyecto. **Fix:** fila de dependencia añadida en ambas tablas + `elapsed_s`/`current_phase` añadidos al schema de guardado (Rule 4, AC-10). + - (c) **Conflicto de modelo de inicio del reloj** (hallazgo nuevo de creative-director, no detectado por ningún especialista): `guardado-persistencia.md` asumía una ventana de Preparación previa al inicio del temporizador; este GDD tiene `PREPARATION` como el estado inicial del reloj desde el instante `ACTIVE`. **Decisión del usuario:** no existe ventana previa — el reloj corre desde `ACTIVE`. Rule 2/AC-05 de `guardado-persistencia.md` reescritas en consecuencia. +3. **[audio-director + game-designer, convergencia independiente] VA-1 citaba un driver sin mapeo definido.** El crescendo decía estar dirigido por `kaiju_timer_remaining_ratio` **e** `imminent_local_progress`, pero solo el segundo (7.5% final del runtime) tenía una curva definida — 92.5% de `PREPARATION` sin escalada especificada. **Fix (ruling de creative-director):** VA-1 reescrita — intensificación continua y sutil dirigida por `kaiju_timer_remaining_ratio` durante toda `PREPARATION`, rampa más marcada en `IMMINENT`, beat discreto opcional de mitad de recorrido para Dirección de Música (reutiliza la señal continua existente, sin nueva API). +4. **[qa-lead] ACs de igualdad de punto flotante fallarían tal como estaban escritas.** AC-T11/T11b afirmaban decimales truncados ("exactamente 0.3333") para fracciones periódicas (1/3, 7/9); AC-T03/T09/T12 afirmaban `0.95` exacto (no representable exactamente en binario). **Fix:** convención de tolerancia ±0.0001 añadida a nivel de documento (sección Formulas); las 5 ACs reescritas. +5. **[systems-designer] Referencia cruzada obsoleta.** Dependencies seguía describiendo Héroes OQ-3 como pendiente de actualizar, aunque ya estaba resuelto (mismo 2026-08-07). **Fix:** fila de dependencia y "Nota de consistencia bidireccional" actualizadas a estado resuelto. + +### DISAGREEMENTS adjudicados por creative-director +- **Legibilidad vs. "dread"**: game-designer argumentó que Rule 5 (informa, nunca emboscar) está en tensión con el framing de "dread" del Player Fantasy. **Ruling: Rule 5 se mantiene sin cambios** — el dread requiere inevitabilidad conocida + incertidumbre de preparación, no un reloj oculto (precedente: Hitchcock, Darkest Dungeon, FTL). Decisión registrada, no reabrir. +- **Variancia de timing por rejugabilidad**: game-designer recomendó jitter entre partidas para evitar memorización. **Ruling: rechazado** — memorizar el reloj es la maestría prevista (Competence loop), no degradación; la palanca correcta para frescura en rejugabilidad es variar *qué* debe lograrse en la ventana (Encuentro con Kaiju), no *cuándo* termina. Decisión registrada, no reabrir. + +### RECOMMENDED y NICE-TO-HAVE (NO aplicados — diferidos, ver revisión completa en el chat de esta sesión) +- Nueva OQ-6 (creative-director): ¿algo amenaza a los héroes antes de `CLIMAX`? Si no, el eje de estaca de H4 casi colapsa (T≈1 siempre). Owner: game-designer, al diseñar Encuentro con Kaiju. +- Elevar el piso seguro de `imminent_window_s` de 30s a 40–45s (margen cero contra la ventana de ~30s de Héroes). +- Advertencia en dev-build para la proporción <20% (hoy silenciosa, AC-T27). +- Conflicto U-1/U-4 (silueta world-space vs. "siempre legible" con cámara RTS pannable) — enrutar a `/ux-design`. +- OQ de arbitraje de audio entre `entered_climax` y los cues A-4/ducking de Héroes — ruling provisional de creative-director: gana el beat de muerte de héroe (Pilar 1 > Pilar 3 en colisión). +- Reinicialización `elapsed_s` entre eras (no bloqueante — multi-era es Vertical Slice, no MVP). +- Diferenciación jugador-perceptible entre la escalada global de IMMINENT y la ventana de ~30s por héroe. +- Techo de frecuencia de flash/strobe para la escalada de IMMINENT (accesibilidad). +- Reescritura conductual de AC-T01 ("verificable por inspección" contradice su tag [Logic/unit]). +- (Nice-to-have adicionales de qa-lead/systems-designer/ux-designer/audio-director — ver transcripción completa de la revisión.) + +**Estado:** 5/5 bloqueantes resueltos. Usuario aceptó las revisiones sin re-review (decisión informada — el usuario reconoce que esto omite verificación independiente de los 5 fixes, incluyendo los 2 que tocaron `datos-de-era-civilizacion.md` y `guardado-persistencia.md`). Marcado **Approved** en `systems-index.md`. diff --git a/design/gdd/reviews/ui-hud-review-log.md b/design/gdd/reviews/ui-hud-review-log.md new file mode 100644 index 0000000000..b57e31e2aa --- /dev/null +++ b/design/gdd/reviews/ui-hud-review-log.md @@ -0,0 +1,24 @@ +# Review Log — UI/HUD + +Historial de revisiones de `design/gdd/ui-hud.md`. Cada entrada registra veredicto, alcance y resolución para que futuras re-revisiones puedan rastrear qué cambió. + +--- + +## Review — 2026-08-14 — Verdict: NEEDS REVISION (revisado y aceptado como Approved el mismo día) +Scope signal: L +Specialists: game-designer, systems-designer, ux-designer, ui-programmer, godot-specialist, qa-lead, creative-director (síntesis senior) +Blocking items: 3 | Recommended: 8 | Nice-to-have: 2 + +**Resumen (síntesis creative-director):** documento estructuralmente sólido (8/8 secciones, bidireccionalidad grep-verificada, contratos bloqueados etiquetados honestamente), pero fallaba en el instante exacto que el sistema existe para servir: la HUD podía quedar en silencio durante la muerte de un héroe. Tres fallos concentrados y arreglables sin rediseño: (1) muerte silenciosa — ráfaga dentro de la ventana de debounce de F1 + muerte fuera de pantalla sin señal (minimapa ausente en MVP, viñeta F3 solo-kaiju); (2) F2 wrong-and-wrong-approach — contradicción de zoom en su propio Edge Case + reimplementación manual de la transformada que derivaría contra `get_screen_center_position()` de Godot 4.6 bajo smoothing de cámara; (3) F3 con ejemplo trabajado ~30% erróneo (0.72, no 0.55) y sin `clamp()` explícito, desbordando 1.10 en el techo del rango seguro de su propio knob `amplitude`. Las tres afirmaciones matemáticas (F3, F2) fueron verificadas independientemente por el revisor principal y por creative-director. + +**Blocking items — resolución:** +1. **Muerte silenciosa** → nuevo Core Rule 6b (director de atención multi-canal: marcador de borde + sting audio/háptico + empujón de cámara para AT_RISK/DYING fuera de pantalla) + camino de disparo instantáneo en F1 (`T_instant`=5.0s salta T_show+debounce). ACs nuevas: AC-U12b (ráfaga), AC-U32 (director fuera de pantalla). HU-11, filas de tabla de estados, Edge Cases reescritos. Decisiones de usuario: los 3 canales del director (no solo uno); `T_instant`=5.0s. +2. **F2** → Edge Case de zoom corregido (`zoom_max`=1.5 es el footprint más pequeño, no `zoom_min`); nota de implementación Godot 4.6 obligatoria (deferir a la transformada de canvas nativa / `get_screen_center_position()`; smoothing/stretch/rotación/DPI); Camera2D confirmado. +3. **F3** → `clamp()` explícito añadido a ambos modos; ejemplo trabajado corregido a ≈0.72 con aritmética completa. + +**Recommended aplicados:** mouse_filter=MOUSE_FILTER_IGNORE (Core Rule 11); fila HIDDEN→DYING promovida a la tabla de estados; AC-U13 reescrita (multi-muestra temporizada) + AC-U13b (reset); re-tags AC-U27→[Logic/BLOCKING], AC-U02→[Integration], AC-U04→[Logic] con ΔE≤2; AC-U08/U29 con barras de aprobación medibles; AC-U31 enumera las 3 animaciones; AC-U33 (legibilidad N-simultánea); regla de tuning conjunto T_show/T_hide/T_instant; contrato de accesibilidad reforzado + aclaración AccessKit vs flash-rate; flag de playtest para corte de tier; OQ-U7 (tasa de cambio de time_to_death) y OQ-U8 (tope de apilamiento/draw calls). + +**Bloqueadas (backfill pendiente upstream):** AC-U17/U18 (verbo de Sacrificio — Combate/Héroes/Control OQ-2; riesgo mayor: la *forma* del mock `order_state` puede resultar incorrecta si upstream aterriza en sacrificio instantáneo), AC-U30 (Reliquias/Bendiciones sin GDD), AC-U31 (Permadeath en In Design, no Approved). + +Prior verdict resolved: First review +Resolución: usuario aceptó las revisiones aplicadas → Status Approved (sin re-review en sesión limpia; ítems bloqueantes verificados aritméticamente). diff --git a/design/gdd/sistema-de-heroes.md b/design/gdd/sistema-de-heroes.md new file mode 100644 index 0000000000..1ea9e2578c --- /dev/null +++ b/design/gdd/sistema-de-heroes.md @@ -0,0 +1,515 @@ +# Sistema de Héroes + +> **Status**: Approved (2026-08-07) — 2 ciclos de design-review + confirmación lean. Recomendados diferidos a vertical slice (ver review-log). **Parche cross-GDD 2026-08-15**: AC-H12/H26 y OQ-4 actualizados para incluir `relic_quality` en el payload de `DYING`; nuevo AC-H27c (coacción DYING→DEAD al recargar); nota de implementación V-5/V-6 (`PROCESS_MODE_ALWAYS`) — cierra los ítems #1, #2 y #5 de `design/gdd/permadeath.md` OQ-P6. No re-abre el veredicto Approved (aditivo, no contradice ninguna decisión previa). +> **Parche cross-GDD 2026-08-17**: rango seguro de `base_accidental` estrechado `0.05–0.15` → `0.10–0.15` (piso inferior). Reconciliación con Forja de Legado (su design-review 2026-08-17): en `D=0` la salida de H4 es exactamente `base_accidental`, así que <0.10 emitiría `relic_quality` fuera del dominio `[0.10,1.00]` de Forja. Aditivo/correctivo, coherente con el "Rango de salida [0.10,1.00]" que este GDD ya declaraba — no re-abre el veredicto Approved. +> **Author**: usuario + agentes +> **Last Updated**: 2026-08-17 +> **Implements Pillar**: Sacrificio con Peso (Pilar 1 — sistema eje); El Pasado es Poder (Pilar 2) + +## Overview + +El Sistema de Héroes define y gobierna las unidades nombradas del jugador: comandantes únicos, cada uno con silueta propia, stats distintivos, y un arco narrativo que culmina —casi siempre— en una muerte permanente y significativa. A nivel de datos, cada héroe es una definición tipada (`HeroDefinition`) con stats superiores a los de las tropas, habilidades propias, un `unit_visual_radius_world` mayor (48-96px de sprite), y —crucialmente— un descriptor del **tipo de reliquia que su muerte producirá** (el "elemento asimétrico" del art bible Sección 5.1, que ancla el Pilar 2). A nivel de jugador, el héroe es el corazón emocional del juego: es la unidad con la que el jugador se encariña sabiendo que su pérdida es probable y, cuando ocurre, permanente e irreversible. A diferencia de las tropas —masa intercambiable cuya muerte es solo una baja— la muerte de un héroe dispara toda la cadena de legado (Permadeath → Forja de Legado → Reliquias → Panteón). Sin este sistema no existiría el Pilar 1: no habría nada cuya pérdida pesara, ni nada que convertir en el poder heredable que define al juego. + +## Player Fantasy + +**El jugador es "El Que Decide Cómo Termina".** No puede salvar a sus héroes — puede decidir para qué mueren. Cada héroe llega ya condenado, y la pregunta que el juego le hace no es "¿sobrevive?", sino "¿esta muerte compró algo, o la desperdiciaste?". Es la fantasía del líder que la Historia recuerda porque supo gastar bien las vidas que no pudo conservar (Pilar 1). + +Esta fantasía resuelve la paradoja del apego-bajo-condena de forma estructural, no tonal: el contrato trágico. Como el público griego que ya conocía el destino de Edipo, el jugador se involucra no por suspenso sobre el resultado sino por inversión en *cómo se lleva*. Por eso Arthas, Cristina y Quirón funcionan — ninguno sorprende, todos se *ganan*. La foreknowledge no mata el apego; lo convierte de esperanza-de-supervivencia en esperanza-de-un-buen-final. + +Dos válvulas mantienen la fantasía honesta: +- **Anti-nihilismo (una muerte se puede desperdiciar)**: si toda muerte produjera una reliquia igual de buena, encariñarse sería irracional. El jugador debe poder malgastar a un héroe —morir en el momento o la posición equivocados, por nada— y obtener una reliquia delgada y triste que lleva su nombre igual. Esto hace que amar bajo condena sea racional, no sentimental. +- **Anti-melodrama (la reliquia siempre es menos)**: un héroe que comandó un ejército se vuelve una espada de tres usos. El juego nunca editorializa que la muerte fue hermosa — sostiene una silueta y te entrega un objeto (Principio C del art bible). El understatement es lo que separa la tragedia de la telenovela. + +El pago emocional del Pilar 2 (el pasado es poder) es la reliquia como *veredicto* sobre cómo gastaste al héroe, no como consuelo automático. Una muerte bien gastada produce una reliquia cuyo poder resuena con la manera de morir; una desperdiciada produce un objeto delgado con el mismo nombre. El legado es una calificación, no un regalo. + +**Tests de diseño (este sistema y sus consumidores deben hacerlos cumplir):** +1. Si la muerte de un héroe está por ocurrir y el jugador no pudo, ~30 segundos antes, tomar una decisión táctica que cambiara *lo que esa muerte produce*, el encuentro se rediseña. La muerte puede ser inevitable; el rendimiento nunca lo es. +2. Debe existir al menos un estado de juego alcanzable por era en el que un héroe muere y el jugador siente que lo desperdició. + +*Criterio de validación (playtest)*: sabremos que el Marco B funcionó si los playtesters, al describir la muerte de un héroe sin que se les pregunte, dicen qué *compró* o *falló en comprar* esa muerte antes de decir cómo los hizo sentir. Si solo reportan tristeza, la fantasía derivó hacia lo melodramático; si reportan frustración/castigo, derivó hacia la culpa pura. + +## Detailed Design + +### Core Rules + +1. **Héroes data-driven con nombre**: cada héroe es una `Resource` tipada (`HeroDefinition`) con: `hero_id`, `hero_name` (nombre propio, no genérico), `max_health` (superior a tropas), `move_speed`, `unit_visual_radius_world` (≥0.5, sprites 48-96px), `abilities` (`Array[AbilityDefinition]`, ≥1 — la estructura interna de `AbilityDefinition` es un contrato del futuro sistema de Habilidades/Combate, sin GDD aún; ver OQ-9), un `asymmetric_element` (descriptor visual, art bible 5.1), y un `relic_category` (`offensive`/`rally`/`defensive`) que la muerte del héroe producirá. Los inputs de fórmula `sprite_diameter_px` y `hero_hp_multiplier`/`pan_speed_fraction` son campos de autoría de los que se derivan `max_health`/`move_speed`/`unit_visual_radius_world` (H1/H2/H3) — ver AC-H01b para su validación al cargar. Leídos del `unit_roster` (porción de héroes) de la era ACTIVE. El MVP maneja **2-3 héroes por era**. +2. **Categoría de reliquia fija, calidad emergente (Pilar 2)**: el `relic_category` de cada héroe es fijo y su `asymmetric_element` lo telegrafía visualmente (un brazo con arma en alto → ofensiva, un estandarte → rally, una reliquia a la espalda → defensiva). Esto le dice al jugador *qué* produciría una muerte bien gastada. La **calidad** de la reliquia resultante NO es fija — depende de las circunstancias de la muerte (ver Interacción con Permadeath). Categoría = telegrafiada; calidad = veredicto. +3. **Prioridad de selección sobre tropas**: los héroes siempre ganan prioridad de selección sobre tropas cercanas (regla ya fijada en Control y Selección). Entre múltiples héroes en el radio, gana el más cercano al clic (desempate ya fijado). +4. **La muerte es permanente e irreversible**: cuando la vida de un héroe llega a 0, transiciona a un estado terminal de muerte que **no puede revertirse** dentro de la partida (sin "cargar antes de la muerte" — Guardado/Persistencia lo hace cumplir con autosave inmediato). La muerte de un héroe **dispara la cadena de legado** vía Permadeath — esta es la distinción mecánica central respecto a las tropas. +5. **Contrato de exposición al morir (para Permadeath)**: al morir, un héroe expone a Permadeath: su `hero_id`, `hero_name`, `relic_category`, y un conjunto de **datos de circunstancia de muerte** — al menos: el tiempo restante en el temporizador del kaiju al morir (`T`), si murió cumpliendo un comando **Sacrificio** deliberado y dentro de `sacrifice_radius` (`D`, ver Fórmula H4 para la definición completa de las tres condiciones), si sirvió un propósito (`P`), y su posición/contexto. Estos datos alimentan el cálculo de *calidad* de la reliquia (válvula anti-nihilismo: una muerte desperdiciada produce baja calidad). *(Contrato provisional — Permadeath, sin GDD aún, debe respetarlo.)* +6. **El jugador siempre pudo cambiar el rendimiento (Test de diseño 1)**: este sistema, junto con Encuentro con Kaiju, debe garantizar que en los ~30s previos a una muerte de héroe inevitable existió una decisión táctica que cambiaba *qué produce* esa muerte. Este sistema aporta la mitad: expone en tiempo real qué circunstancias mejorarían/empeorarían la reliquia, para que la decisión sea legible. + +### States and Transitions + +| Estado | Descripción | Transición desde | Transición hacia | +|---|---|---|---| +| `ALIVE_IDLE` | En el campo, sin orden, vivo | Instanciación, llegada, fin de combate | `ALIVE_MOVING`, `ALIVE_ENGAGED`, `DYING` | +| `ALIVE_MOVING` | Ejecutando pathfinding hacia un destino | Orden de movimiento | `ALIVE_IDLE`, `ALIVE_ENGAGED`, `DYING` | +| `ALIVE_ENGAGED` | En combate (delegado a Combate/Daño) | Contacto/orden de ataque | `ALIVE_IDLE`, `ALIVE_MOVING`, `DYING` | +| `DYING` | Vida llegó a 0 — beat de muerte sostenido en curso (Principio C) | Cualquier estado ALIVE al llegar vida a 0 | `DEAD` (al liberarse el beat) | +| `DEAD` | Muerte confirmada, reliquia forjada, removido del campo | `DYING` | — (terminal, irreversible) | + +- **`DYING` es especial**: dispara el contexto `DEATH_HOLD` del Input (que suprime todo input), sostiene el beat visual (art bible 2.3), y solo tras liberarse el beat se confirma `DEAD` y se dispara la Forja de Legado. Este sistema no controla el timing del beat — Permadeath sí (ya fijado en el contrato de Input). +- La transición `DYING → DEAD` es el momento en que se expone el contrato de circunstancia de muerte a Permadeath. + +### Interactions with Other Systems + +| Sistema | Dirección | Interfaz | +|---|---|---| +| Datos de Era/Civilización | Datos → este sistema | Lee `unit_roster` (porción de héroes) de la era ACTIVE | +| Control y Selección de Unidades | bidireccional | Provee posición/hitbox y `unit_visual_radius_world` (≥0.5, > tropas) para selección con prioridad; recibe órdenes | +| Combate/Daño | bidireccional | Delega cálculo de daño; recibe eventos que reducen vida; notifica `ALIVE_ENGAGED`/`DYING` | +| Permadeath | este sistema → Permadeath | Al entrar a `DYING`, expone `hero_id`, `hero_name`, `relic_category`, `relic_quality` (evaluada vía Fórmula H4 en el frame del snapshot), y D/T/P snapshoteados como evidencia; también notifica el fin de la animación de resolución (V-5→V-6) que alimenta `animation_done` de Permadeath. Permadeath controla el timing del beat y confirma `DEAD` | +| Encuentro con Kaiju | este sistema → Kaiju | Los héroes son objetivos de alto valor para la IA del kaiju; expone posiciones; comparte el garantizar el Test de diseño 1 (ventana de decisión) | +| Guardado/Persistencia | este sistema → Guardado | La muerte de un héroe dispara autosave obligatorio inmediato (hace irreversible la muerte, Pilar 1) | + +*(Nota provisional: Combate/Daño, Permadeath y Encuentro con Kaiju aún no tienen GDD — contratos propuestos. El contrato con Permadeath es el más crítico. Datos de Era/Civilización, Control y Selección y Guardado/Persistencia están confirmados.)* + +## Formulas + +*(Los 3 arquetipos —Vanguardia/Portaestandarte/Guardián— son ilustrativos de las 3 categorías de reliquia, no una lista de héroes cerrada; los rosters reales por era viven en Datos de Era/Civilización.)* + +### Formula H1 — `hero_max_health` + +`hero_max_health = melee_troop_max_health × hero_hp_multiplier` + +Anclado a la tropa más resistente (melee, 40 HP) para que todo héroe supere visiblemente a toda tropa por el multiplicador, sin excepciones. + +| Variable | Símbolo | Tipo | Rango | Descripción | +|---|---|---|---|---| +| Vida de tropa melee | `melee_troop_max_health` | int (const) | 40 (registrado) | Ancla — la tropa más resistente | +| Multiplicador de vida | `hero_hp_multiplier` | float | 3.0–5.0 | Por arquetipo; bajo para agresivos/frágiles, alto para ancla/defensivos | +| **Vida de héroe** | `hero_max_health` | int | 120–200 | Pool máximo de vida | + +| Arquetipo | relic_category | multiplicador | hero_max_health | +|---|---|---|---| +| Vanguardia | offensive | 3.5× | 140 | +| Portaestandarte | rally | 3.75× | 150 | +| Guardián | defensive | 5.0× | 200 | + +### Formula H2 — `hero_move_speed` + +`hero_move_speed = camera_pan_speed × pan_speed_fraction` (mismo patrón registrado para tropas). + +| Variable | Símbolo | Tipo | Rango | Descripción | +|---|---|---|---|---| +| Velocidad de paneo | `camera_pan_speed` | float (const) | 30 (registrado) | Ancla de velocidad | +| Fracción por arquetipo | `pan_speed_fraction` | float | 0.10–0.15 | Los héroes no deben superar a la tropa más rápida (ranged @ 0.167) | +| **Velocidad de héroe** | `hero_move_speed` | float | 3.0–4.5 u/s | Salida | + +Ejemplos: Vanguardia 0.1417→4.25; Portaestandarte 0.1333→4.0; Guardián 0.1167→3.5. Todos por debajo de camera_pan_speed (30) y de la tropa más rápida (5.0). + +### Formula H3 — `hero_unit_visual_radius_world` + +`unit_visual_radius_world = sprite_diameter_px / world_unit_scale / 2`. El piso `≥ 0.5` **no** se garantiza con un clamp de salida sino con una **validación de entrada**: `sprite_diameter_px` debe ser `≥ 48`, y un valor menor **falla ruidosamente al cargar** (no se clampa silenciosamente — coherente con la política loud-fail de Tuning Knobs y con AC-H02). Con la entrada validada, la salida es siempre `≥ 0.5` sin clamp. + +| Variable | Símbolo | Tipo | Rango | Descripción | +|---|---|---|---|---| +| Diámetro de sprite | `sprite_diameter_px` | int | 48–96 | Sprite de héroe (art bible LOD 2). `< 48` = error de autoría (loud-fail), nunca produce radio < 0.5 | +| Escala de mundo | `world_unit_scale` | int (const) | 48 (registrado) | px por world unit | +| **Radio visual** | `unit_visual_radius_world` | float | 0.5–1.0 | Siempre ≥0.5 (por validación de entrada, no clamp de salida) y >0.4583 (radio de tropa mayor) | + +Ejemplos: Vanguardia 64px→0.6667; Portaestandarte 72px→0.7500; Guardián 80px→0.8333. Todos superan el techo de tropa (0.4583), manteniendo la prioridad héroe>tropa. + +### Formula H4 (CLAVE) — `relic_quality` (versión gated) + +El corazón mecánico del Marco B y de la válvula anti-nihilismo. La **intención** del jugador (`D`) es una compuerta: una muerte accidental queda topada al piso sin importar la escena; solo una muerte deliberada desbloquea los bonos de estaca (`T`) y propósito (`P`). + +``` +relic_quality = clamp( + base_accidental + D × (base_deliberate − base_accidental + w_stakes × T + w_purpose × P), + 0.0, 1.0 +) +``` + +| Variable | Símbolo | Tipo | Rango | Descripción | +|---|---|---|---|---| +| ¿Deliberada? | `D` (`was_deliberate`) | bool (0/1) | {0,1} | `D=1` sii se cumplen las **tres** condiciones: (1) el jugador emitió un comando **Sacrificio** dedicado (verbo con confirm-step, no un `hold`/`guard` reflejo) sobre este héroe; (2) ese comando seguía **activo/no-cancelado** en el frame de muerte; (3) la muerte ocurrió **dentro de `sacrifice_radius`** del destino ordenado. En cualquier otro caso `D=0`. El radio tolera el desplazamiento por knockback del kaiju (una muerte legítima empujada fuera del punto sigue contando); el confirm-step evita el farmeo reflejo. *Contrato: Combate/Daño provee la clasificación aplicando esta definición.* | +| Estaca del kaiju | `T` (`kaiju_stakes`) | float | [0,1] | `1 − kaiju_timer_remaining_ratio`. 0=murió en una calma, 1=murió en el clímax exacto. *Contrato: `kaiju_timer_remaining_ratio` lo **posee** el Temporizador de Preparación/Ritual (Regla 3 / Fórmula F2 de ese GDD); Encuentro con Kaiju lo consume en paralelo. Resuelto — ver OQ-3.* | +| ¿Propósito? | `P` (`served_purpose`) | bool (0/1) | {0,1} | `P=1` si la muerte sirvió a un propósito táctico marcado, por cualquiera de dos vías (equivalentes, para no privilegiar arquetipos defensivos — ver nota de arquetipos): **(defensiva)** murió dentro de `guard_radius` de un objetivo/aliado crítico marcado, **o (ofensiva)** murió eliminando/neutralizando una amenaza crítica marcada del kaiju (parte/esbirro señalado). *Contrato: Combate/Kaiju provee la clasificación de ambas vías.* | +| Piso accidental | `base_accidental` | float (const) | 0.10–0.15 (MVP: **0.10**) | Piso de una muerte desperdiciada — garantiza que siempre se forja una reliquia (nunca 0), pero delgada. **Rango estrechado 0.05→0.10 en el piso inferior (2026-08-17):** como en `D=0` la salida es exactamente `base_accidental`, un valor <0.10 emitiría `relic_quality` por debajo del dominio `[0.10,1.00]` que Forja de Legado asume, disparando su loud-fail en cada muerte accidental (Forja design-review 2026-08-17). El piso 0.10 es también coherente con el "Rango de salida [0.10,1.00]" ya declarado abajo | +| Piso deliberado | `base_deliberate` | float (const) | 0.35–0.50 (MVP: **0.40**) | Piso una vez establecida la intención. El floor **0.35** (no 0.30) es load-bearing: en `T=0,P=0` la fórmula da exactamente `base_deliberate`, así que un valor <0.35 haría colisionar una muerte deliberada (D=1) con una accidental en Tier 1 — se pierde la compuerta. Ver Enforcement y AC-H23d | +| Peso de estaca | `w_stakes` | float (const) | 0.20–0.40 (MVP: **0.35**) | Bono máximo por morir en el momento climático | +| Peso de propósito | `w_purpose` | float (const) | 0.15–0.30 (MVP: **0.25**) | Bono máximo por morir defendiendo un objetivo | +| **Calidad de reliquia** | `relic_quality` | float | **[0.10, 1.00]** | Consumido por Permadeath → Forja de Legado para fijar tier/fuerza de reliquia | + +**Rango de salida**: [0.10, 1.00], nunca 0 — el `base_accidental` es un piso incondicional. Esto hace cumplir la línea de la fantasía: "una reliquia delgada y triste que lleva su nombre igual" — una muerte se puede desperdiciar, pero nunca produce *nada*. + +**Mapeo de tiers** (para que Forja de Legado lo consuma): + +| Tier | Rango de `relic_quality` | Etiqueta | +|---|---|---| +| 1 — Delgada | [0.10, 0.35) | Reliquia delgada | +| 2 — Sólida | [0.35, 0.70) | Reliquia sólida | +| 3 — Legendaria | [0.70, 1.00] | Reliquia legendaria | + +**Ejemplos:** +1. **Desperdiciada** — héroe idle, sin orden, abatido lejos de todo. `D=0` → `relic_quality = 0.10` → **Tier 1 Delgada.** +2. **Deliberada pero temprana** — orden de Hold al inicio (timer recién reseteado), lejos de objetivos. `D=1, T=0.05, P=0` → `0.10 + (0.30 + 0.35×0.05 + 0) = 0.4175` → **Tier 2 Sólida.** +3. **Bien gastada, climática** — el jugador compromete al héroe a sostener un cuello de botella justo cuando el timer del kaiju toca fondo, tanqueando un golpe destinado a un grupo de tropas en retirada. `D=1, T=0.95, P=1` → `0.10 + (0.30 + 0.35×0.95 + 0.25×1) = 0.9825` → **Tier 3 Legendaria.** + +Esto hace legible el Test de diseño 1 en tiempo real: al emitir (o no) el comando **Sacrificio** sobre un héroe condenado, y conforme el timer del kaiju baja, el readout de UI de este score puede cambiar de estado visiblemente o quedarse plano — mostrando qué produciría la muerte en ese momento, ~30s antes, sin narrarlo (ver UI Requirements U-1..U-4). + +**Nota de simetría entre arquetipos**: `P` tiene dos vías equivalentes (defensiva = morir en `guard_radius` de un objetivo; ofensiva = morir neutralizando una amenaza marcada del kaiju) precisamente para que un héroe ofensivo (Vanguardia) pueda alcanzar Tier 3 sin verse forzado a morir junto a un objetivo defensivo. Sin esa segunda vía, con `P=0` el techo en `D=1` sería `base_deliberate + w_stakes = 0.75`, alcanzando Legendaria solo con `T ≥ 0.857` (los últimos ~14% del timer) — un embudo que empujaría a todos los héroes a la misma estrategia de "parkear junto a un objetivo antes de sacrificar", homogeneizando arquetipos. *(Alternativa registrada para playtest: pesos `w_stakes`/`w_purpose` relativos al arquetipo. Se prefirió la vía dual de `P` por ser más simple y no multiplicar el espacio de tuning — ver OQ-7.)* + +**Nota de identidad de diseño (decisión consciente)**: `D`/`T`/`P` son idénticos en tipo y peso para todo héroe — la fórmula NO varía por identidad de héroe. Es una decisión deliberada (escuela Hades: sistemas legibles y aprendibles por sobre casos especiales bespoke). La carga *narrativa/trágica* de cada muerte no proviene de la fórmula sino del **contexto autoral** (quién era el héroe, qué exigía la era, su `asymmetric_element` y `hero_name`) superpuesto a un ritual mecánico limpio. Se descartó para el MVP añadir inputs que varíen por héroe individual, para no convertir la muerte en un puzzle de optimización per-héroe. + +### Formula H5 — Umbral de muerte + +`health_current ≤ 0 → transition(ALIVE_*, DYING)` + +| Variable | Símbolo | Tipo | Rango | Descripción | +|---|---|---|---|---| +| Vida actual | `health_current` | int | 0–hero_max_health | HP vivo del héroe | + +**Distinción de las tropas**: el umbral es mecánicamente idéntico, pero al cruzarlo un héroe **snapshotea** los inputs D/T/P (no los lee tarde, ya que T y P varían en el tiempo) y los congela en el payload de circunstancia de muerte expuesto a Permadeath en la transición `DYING → DEAD` — este sistema evalúa la Fórmula H4 en ese mismo frame y agrega `relic_quality` ya computada al payload (Permadeath no la recalcula, solo la transporta). (Ver Edge Cases — el timing del snapshot es una fuente común de bugs de race-condition.) + +## Edge Cases + +- **Si un héroe entra a `DYING` (vida cruza 0)**: los inputs `D`/`T`/`P` de `relic_quality` se **snapshotean y congelan en ese frame exacto**, no se leen tarde. `T` (estaca del kaiju) y `P` (propósito/proximidad) varían en el tiempo — leerlos después del frame de muerte produciría una calidad de reliquia incorrecta. `D` también se captura en ese frame desde el estado de comando almacenado (¿comando Sacrificio activo y dentro de `sacrifice_radius`?), no muestreado continuamente — un comando cancelado el instante anterior/posterior al golpe letal se resuelve por el estado en el frame de cruce, coherente con AC-H21c. La captura ocurre en el cruce por 0, antes de que el beat sostenido inicie. +- **Si dos héroes entran a `DYING` en el mismo frame**: cada uno snapshotea sus propios `D`/`T`/`P` independientemente y expone su propio contrato a Permadeath. Permadeath serializa los beats de muerte (no dos beats sostenidos simultáneos) — el orden de resolución lo define Permadeath, no este sistema. +- **Si un héroe muere por overkill** (golpe que lo lleva muy por debajo de 0): Combate/Daño hace clamp de la vida a 0; la transición a `DYING` dispara igual en el cruce por 0. El monto de overkill no afecta `relic_quality` (no es un input de la fórmula). +- **Si un héroe muere sin ningún comando Sacrificio activo** (`D=0`): produce `relic_quality`=0.10 (Tier 1 Delgada) — la muerte no se desperdicia en el sentido de "no producir nada", pero sí es la reliquia más pobre. Es la válvula anti-nihilismo funcionando, no un bug. (Un `hold`/`guard` ordinario **no** cuenta como Sacrificio — solo el comando dedicado con confirm-step, ver Fórmula H4.) +- **Si el jugador emite un comando Sacrificio pero el héroe muere fuera de `sacrifice_radius` del destino**: `D=0` — la muerte no ocurrió donde se comprometió al héroe. Esto evita "gamear" el sistema comprometiendo héroes lejos del combate real. +- **Si un héroe con comando Sacrificio activo es empujado por knockback del kaiju y muere DENTRO de `sacrifice_radius` del destino**: `D=1` — el radio tolera el desplazamiento por knockback/pathfinding, de modo que un sacrificio genuino no se penaliza por mala suerte física. Este es el caso que la definición de exact-match anterior rompía. (Fuera del radio → `D=0`, caso anterior.) +- **Si el comando Sacrificio se canceló/reemplazó antes del frame de muerte**: `D=0` — la condición (2) exige que el comando siga activo al morir. El jugador cambió de intención antes de que la muerte ocurriera. +- **Si el jugador toca Sacrificio reflejamente sobre un héroe condenado sin confirmar**: el comando no se emite — el confirm-step es parte del comando (ver Fórmula H4 y contrato con Combate/Daño). Un tap sin confirmación no produce `D=1`. Esto cierra el farmeo por reflejo. +- **Si un héroe seleccionado entra a `DYING`**: se remueve del conjunto de selección de Control y Selección (su AC-18/19 lo cubre). Durante `DYING`, el Input está en `DEATH_HOLD` y suprime todo input, así que el jugador no puede seleccionar/comandar nada hasta que el beat se libere. +- **Si todos los héroes de la era mueren**: es (casi con certeza) la condición de derrota de la era — pero la evaluación de victoria/derrota **no la posee este sistema** (mismo hueco abierto que en Tropas). Este sistema solo notifica cada muerte; quién declara la derrota es Encuentro con Kaiju o Transición de Era. +- **Si un `HeroDefinition` no declara `relic_category`** (error de datos): falla ruidosamente al cargar en builds de desarrollo — un héroe sin categoría de reliquia rompe el Pilar 2. Nunca se asigna una categoría por defecto silenciosamente. +- **Si el beat de muerte sostenido (`DYING`) es interrumpido por un crash/cierre**: al reiniciar, el autosave obligatorio ya disparado por Guardado/Persistencia (Regla 4) garantiza que el héroe está muerto y la reliquia forjada — el jugador no puede reaparecer con el héroe vivo recargando. La muerte es irreversible incluso a través de un crash. + +## Dependencies + +**Dependencias hacia arriba (upstream):** + +| Sistema | Tipo | Interfaz | Estado | +|---|---|---|---| +| Datos de Era/Civilización | Dura | Lee `unit_roster` (porción de héroes) de la era ACTIVE | ✅ Diseñado | +| Control y Selección de Unidades | Dura (bidireccional) | Provee posición/hitbox y `unit_visual_radius_world` (≥0.5, > tropas); recibe órdenes con prioridad héroe>tropa | ✅ Diseñado — contrato confirmado | + +**Dependientes hacia abajo (downstream):** + +| Sistema | Tipo | Interfaz | +|---|---|---| +| Permadeath | Dura (**consumidor crítico**) | Recibe el contrato de circunstancia de muerte (`hero_id`, `hero_name`, `relic_category`, `relic_quality` ya computada, D/T/P snapshoteados); controla el timing del beat `DEATH_HOLD` y confirma `DEAD` | +| Combate/Daño | Dura (bidireccional) | Delega cálculo de daño; recibe eventos que reducen vida; notifica `ALIVE_ENGAGED`/`DYING`. Provee la clasificación `D` (deliberada) y `P` (protegió objetivo) | +| Temporizador de Preparación/Ritual | Dura | **Dueño** de `kaiju_timer_remaining_ratio` (input `T`); este sistema lo lee en el frame de muerte. ✅ Diseñado — resuelve OQ-3 | +| Encuentro con Kaiju | Dura | ✅ **Approved** (`encuentro-con-kaiju.md`). Los héroes son objetivos de alto valor de la IA del kaiju (Regla 3); Encuentro provee `time_to_death` (F2) **e** `incoming_damage_rate_total` (F6, kaiju+esbirros) por héroe para el trigger del readout (U-3/AC-H29/AC-H32b) — el readout refleja la fuente más peligrosa (Encuentro Regla 6b/AC-K68); el marcado `P` (defensiva/ofensiva) lo produce Encuentro Regla 5. **Consume** (no posee) `kaiju_timer_remaining_ratio` | +| Guardado/Persistencia | Dura | La muerte de un héroe dispara autosave obligatorio inmediato (hace irreversible la muerte, Pilar 1) | +| UI/HUD | Dura | Consume el tier proyectado de `relic_quality` (U-1/U-2), el snapshot D/T/P congelado en `DYING` (U-4), la prioridad de selección héroe>tropa (U-6/AC-H33) y `asymmetric_element`/`relic_category` como ancla visual del readout de veredicto (U-1..U-7). *En diseño (`design/gdd/ui-hud.md`)* | +| Reliquias/Bendiciones | Dura (modificadores de instancia) | Las bendiciones aplican modificadores planos a instancias de héroe vía la capa `CombatState` de Combate (ADR-0001); un modificador activo se descarta con la instancia al entrar el héroe a `DYING` (Reliquias AC-RB41) y **no** altera la clasificación D/T/P de la muerte. El targeting single-hero de la bendición sigue pendiente (Reliquias OQ-RB7) | + +**Nota bidireccional (actualizada 2026-08-21)**: Permadeath, Combate/Daño, Encuentro con Kaiju y Reliquias/Bendiciones **ya tienen GDD** — sus contratos están confirmados (la fila recíproca de Reliquias se añadió en la reconciliación cross-GDD, cierra parte de Reliquias OQ-RB3). El contrato con Permadeath sigue siendo el más crítico del proyecto (define cómo la muerte se convierte en legado). Datos de Era/Civilización, Control y Selección y Guardado/Persistencia están confirmados. + +## Tuning Knobs + +| Knob | Símbolo | Tipo | Default | Rango seguro | Qué se rompe en extremos | +|---|---|---|---|---|---| +| Piso de calidad accidental | `base_accidental` | float | 0.10 | 0.10–0.15 | <0.10: **loud-fail de contrato** — emitiría `relic_quality` bajo el dominio `[0.10,1.00]` de Forja de Legado (en `D=0` la salida ES `base_accidental`), rompiendo el piso anti-nihilismo compartido; >0.15: desperdiciar deja de castigar, se diluye la válvula anti-nihilismo. *(Piso inferior estrechado 0.05→0.10 en reconciliación con Forja, 2026-08-17)* | +| Piso de calidad deliberada | `base_deliberate` | float | 0.40 | 0.35–0.50 | <0.35: una muerte deliberada sin estaca ni propósito (T=0,P=0) colisiona en Tier 1 con una accidental — se pierde la garantía de la compuerta (loud-fail al cargar); >0.50: la intención sola casi alcanza Legendaria, los bonos de clímax/objetivo pierden peso | +| Peso de estaca (timer kaiju) | `w_stakes` | float | 0.35 | 0.20–0.40 | <0.20: morir en el clímax no importa; >0.40: solo el timing determina todo, el propósito se vuelve irrelevante | +| Peso de propósito (objetivo) | `w_purpose` | float | 0.25 | 0.15–0.30 | <0.15: proteger un objetivo no importa; >0.30: eclipsa la estaca del timer | +| Multiplicador de vida de héroe | `hero_hp_multiplier` | float | 3.0–5.0 (por arquetipo) | 3.0–5.0 | <3.0: los héroes no se sienten más resistentes que las tropas; >5.0: se vuelven casi inmortales, se pierde el peso de la muerte | +| Fracción de velocidad de héroe | `pan_speed_fraction` | float | por arquetipo | 0.10–0.15 | <0.10: héroes se arrastran; >0.15: alcanzan/superan a la tropa más rápida (ranged 5.0 u/s), rompiendo el techo de velocidad | +| Diámetro de sprite de héroe | `sprite_diameter_px` | int | por arquetipo | 48–96 | <48 (→radio <0.5): **loud-fail al cargar** (no se clampa) — perdería el piso de radio y la separación héroe>tropa; >96: **loud-fail** — sprite desproporcionado a escala RTS | +| Radio de cumplimiento del Sacrificio | `sacrifice_radius` | float | 3.0 | 2.0–5.0 | Radio alrededor del destino ordenado dentro del cual una muerte cumple el comando Sacrificio (input de `D`). <2.0: el knockback del kaiju hace fallar sacrificios legítimos (D=0 por mala suerte); >5.0: casi cualquier muerte cerca cuenta, se diluye la deliberación | +| Radio de guardia del propósito | `guard_radius` | float | 4.0 | 2.5–6.0 | Radio alrededor de un objetivo/aliado marcado dentro del cual una muerte lo protege (vía defensiva de `P`). <2.5: proteger es casi imposible; >6.0: estar vagamente cerca ya cuenta. *Valor final confirmado con Combate/Kaiju (OQ-2).* | + +**Interacción crítica (ENFORZADA, no solo recomendada)**: en `D=1` la fórmula se reduce a `relic_quality = base_deliberate + w_stakes×T + w_purpose×P` (el término `base_accidental` se cancela). Por eso la **suma** `base_deliberate + w_stakes + w_purpose` es load-bearing para la reachability de Tier 3. Con los floors individuales actuales (base_deliberate≥0.35, w_stakes≥0.20, w_purpose≥0.15) el mínimo conjunto es **exactamente 0.70**, así que la mejor muerte posible (D=1,T=1,P=1) llega justo al umbral de Tier 3 Legendaria (≥0.70) — apenas alcanzable, que es la intención. (Nota histórica: sin el floor de base_deliberate=0.35 introducido en re-review, el mínimo conjunto habría sido 0.30+0.20+0.15 = 0.65 y Tier 3 habría sido **inalcanzable** — por eso el floor individual y el piso de suma trabajan juntos, ver "Segundo constraint" abajo.) En el otro extremo, el máximo individual (0.50+0.40+0.30 = 1.20) alcanzaría Legendaria con T=0 (muerte en calma), contradiciendo la fantasía. Por eso la suma se **valida y restringe a `[0.70, 1.00]`**: el piso garantiza que Legendaria sea alcanzable, el techo que siga exigiendo una muerte bien gastada. Los defaults (0.40+0.35+0.25 = 1.00) cumplen. + +**Segundo constraint, independiente del anterior**: en el extremo `T=0,P=0` los términos de peso se anulan y `relic_quality = base_deliberate` exactamente. Si `base_deliberate` cayera por debajo de 0.35, una muerte **deliberada** (D=1) pero sin estaca ni propósito produciría <0.35 → **Tier 1 Delgada**, idéntica en tier a una muerte accidental (D=0 → `base_accidental`). Eso colapsaría la compuerta que la fórmula existe para sostener. La restricción de la suma `[0.70,1.00]` **no** atrapa este caso (solo inspecciona la suma, no el término individual), por lo que `base_deliberate ≥ 0.35` es un enforcement individual separado (ver Enforcement punto 3 y AC-H23d). + +**Enforcement** (política loud-fail uniforme — nunca se sustituye silenciosamente un valor de config, coherente con AC-H02): +1. **Cada knob fuera de su rango seguro individual falla ruidosamente al cargar** (error/assert en builds de desarrollo) — **no** se clampa al límite más cercano. Clampar en silencio un constante load-bearing (p. ej. `base_deliberate` 0.60→0.50, un swing de 0.10 que mueve los boundaries de tier) ocultaría exactamente el tipo de error que este enforcement existe para atrapar. Ver AC-H23. +2. **Adicionalmente**, si `base_deliberate + w_stakes + w_purpose ∉ [0.70, 1.00]`, la carga también falla ruidosamente — la suma es load-bearing para la reachability de tiers aunque cada término esté en rango. Ver AC-H23b/AC-H23c. +3. El floor individual `base_deliberate ≥ 0.35` (punto 1) es doblemente load-bearing: garantiza que una muerte deliberada (D=1) con `T=0,P=0` produzca ≥0.35 (Tier 2 Sólida), nunca colisionando en Tier 1 con una muerte accidental (que da exactamente `base_accidental`). Ver AC-H23d. +4. **Aislamiento de test**: la configuración se carga en una instancia inyectable/aislada por test, no en un singleton global mutable — un test de config corrupta (AC-H23/b/c/d) no debe contaminar el estado de los tests de fórmula canónica (AC-H13–H20). Coherente con el estándar de Isolation del proyecto. + +**Constantes consumidas** (no se duplican): `camera_pan_speed`=30, `world_unit_scale`=48, `melee_troop_max_health`=40 (de `melee_infantry`) → propiedad de otros sistemas (registradas). + +## Visual/Audio Requirements + +*Referencia rectora: Art Bible "What the Gods Left Behind" — Secciones 2.3 (Muerte/Sacrificio), 5.1 (arquetipo de héroe), 5.2 (rasgo distintivo), 5.3 (pose), 5.4 (LOD). Los requisitos de audio son un **contrato provisional** (sin dirección de audio ni audio bible aún) — deben re-verificarse cuando exista.* + +### Visual + +**V-1 — Silueta de héroe con elemento asimétrico único (art bible 5.1)** +Cada héroe se construye alrededor de **un solo elemento asimétrico ornamentado** (brazo-arma-en-alto, capa que arrastra, estandarte fuera de eje, reliquia a la espalda); el resto de la silueta se mantiene plano/funcional. Ese elemento es el ancla visual de la futura reliquia y el único punto donde se concentra detalle. **No usa Oro Reliquia en vida** — solo acentos Hueso/Piedra Fría/Ocre (el dorado se reserva para el prop de reliquia post-muerte). + +**V-2 — El elemento asimétrico telegrafía la `relic_category` (art bible 5.2, Regla Core 2)** +El *tipo* de elemento asimétrico debe corresponder a la `relic_category` del `HeroDefinition`: brazo-arma-en-alto → `offensive`; estandarte → `rally`; reliquia heredada a la espalda → `defensive`. Es la lectura visual de "qué produciría una muerte bien gastada", legible desde el reclutamiento. + +**V-3 — Legibilidad a escala RTS y separación héroe > tropa (art bible 3, 5.2)** +La silueta del héroe debe leerse como "un héroe" a 24-32px por la *presencia* del elemento asimétrico (las tropas carecen de él — regla dura, sin excepciones). Diferenciación héroe-vs-héroe **solo** por cuál es el elemento asimétrico y dónde se ubica, nunca alterando la gramática vertical base. + +**V-4 — Dos niveles de LOD, Nivel 2 exclusivo de permadeath (art bible 5.4)** +Dos LOD fijos, no continuos: +- **Nivel 1 (juego)**: silueta + bloque de color; sin detalle facial/ornamento fino. +- **Nivel 2 (detalle pleno pictórico)**: activado **solo** por (a) el beat sostenido `DYING` (2.3), (b) retrato del Salón Conmemorativo, (c) revelación de reliquia. Ninguna acción rutinaria de héroe, muerte de tropa ni idle activa Nivel 2. Se requieren **3 assets de Nivel 2 por héroe** (beat de muerte, retrato memorial, prop de reliquia dorada). + +**V-5 — Beat de muerte sostenido (art bible 2.3, 5.3 excepción, Principio C)** +Al entrar a `DYING`, la escena se compone según el Principio C — silueta legible sin obstrucción, pausa compositiva deliberada, **sin corte a negro ni cutscene**. Iluminación: cambio súbito a una única luz clave cálida ("luz divina") que aísla al héroe mientras el combate circundante se desatura/desenfoca; casi-silueta a contraluz. **Pose**: única excepción a la rigidez estatuaria — silueta dinámica de colapso/alcance que carga toda la legibilidad emocional sin rostro ni diálogo. + +**V-6 — Resolución del beat: silueta → reliquia (art bible 2.3, Principio A)** +La silueta del héroe se **agrieta en fragmentos de vitral tipo rosetón**, cada fragmento congelado un instante en el aire antes de resolverse en el ícono de la reliquia con borde dorado (Oro Reliquia — su primer momento de render material pleno). Es la transición visual que acompaña `DYING → DEAD`. + +**Nota de implementación (contrato con Permadeath, Regla 4 / OQ-P6 #5):** el nodo que reproduce V-5→V-6 debe correr exento de la pausa cooperativa que Permadeath activa durante el beat (`game_time_paused`) — en Godot 4.6, `process_mode = PROCESS_MODE_ALWAYS` (o equivalente), igual que el nodo orquestador de Permadeath. Si esta animación se congelara junto con el resto del mundo pausado, su señal de fin (`animation_done`, consumida por la Fórmula P1 de Permadeath) nunca llegaría y todo beat caería al timeout de seguridad, enmascarando el camino normal (Permadeath AC-P04). Aplica también a los nodos de partícula/audio/tween que impulsan el efecto de vitral, no solo al nodo raíz de la animación. + +**V-7 — Radio visual mayor que tropas (Fórmula H3)** +Sprite de héroe 48-96px → `unit_visual_radius_world` en [0.5, 1.0], siempre > 0.4583 (techo de tropa), sosteniendo la prioridad de selección héroe > tropa también a nivel de hitbox visual. + +### Audio *(contrato provisional — sin audio bible)* + +**A-1 — Tratamiento sonoro del beat sostenido** +El beat `DYING` requiere un tratamiento de audio propio que subraye la pausa del Principio C: caída/filtrado del lecho de combate (el mundo "se aparta"), acentuando el aislamiento del héroe. El *timing* lo controla Permadeath (mismo dueño del beat visual), no este sistema — cuando exista su GDD, el audio debe sincronizarse a sus hit-points (crack de silueta V-6, revelación de reliquia). Contrato con la futura dirección de audio. + +**A-2 — Understatement sonoro (Pilar anti-melodrama); el silencio es una herramienta válida** +El audio de muerte **no debe editorializar** que la muerte fue hermosa (paralelo sonoro del Principio C / anti-melodrama): contención, no swell triunfal. **Silencio/casi-silencio (dead air) es una opción explícitamente permitida** — puede ser la forma más fuerte de understatement, no se asume que siempre haya material compuesto activo. *(Nota: no se presupone idioma/instrumentación —"swell orquestal" es solo un ejemplo de lo prohibido, no una afirmación de que exista una orquesta; esa decisión pertenece a Dirección de Música.)* + +**A-3 — Ciego al RESULTADO (tier), no ciego a la IDENTIDAD** +El audio del beat **no** anticipa ni "califica" sonoramente el *tier emergente* de la reliquia (Delgada/Sólida/Legendaria) — la calidad es un veredicto legible en la UI (U-1..U-2), no un premio auditivo; esto protege el understatement y el test negativo de agencia. **PERO esto es outcome-blind, no identity-blind**: sí se permite (y se recomienda, para que cada muerte se sienta suya y evitar fatiga de repetición a lo largo de decenas de muertes) variación por **identidad fija del héroe** — su `relic_category` (offensive/rally/defensive, telegrafiada desde el reclutamiento) o rasgos propios. Variar por algo que el jugador ya sabía de antemano no premia un resultado emergente; repetir un audio idéntico en cada muerte sí aplanaría el beat central del juego. + +**Restricción negativa (para que "identity-varied" no se filtre a "outcome-signaling")**: `relic_category` **no** es outcome-neutral en la práctica — la nota de simetría de H4 admite que los arquetipos defensivos alcanzan `P=1` más fácil, y difieren en tankiness (H1). Por eso la variación por identidad **no debe correlacionar su peso/intensidad acústica con la distribución típica de `relic_quality` del arquetipo**: p. ej. dar al Guardián un cue de muerte más pesado/resonante "por identidad" enseñaría, a lo largo de decenas de muertes, una correlación identidad→tier — exactamente lo que A-3 prohíbe. La variación permitida es *tímbrica/temática* (que suene a *ese* héroe), no *graduada en magnitud* de un modo que rastree el tier. "Rasgos propios" se limita a atributos fijos y conocidos de antemano, nunca a proxies del resultado. + +**A-4 — Audio de la ventana de decisión (no solo del epílogo)** +Los inputs D/T/P se snapshotean en `health≤0`, *antes* de que `DYING` empiece — así que A-1 (audio del beat) llega cuando el resultado ya está sellado. La **ventana de decisión de ~30s** (Test de diseño 1) necesita su propio canal auditivo, porque en un RTS ocupado el sonido es el mejor canal periférico: un **cue discreto en cada cruce de tier del readout** (empareja el cambio de estado de U-2, canal de audio/haptic de U-1) le dice al jugador "tu decisión acaba de cambiar el veredicto" sin que tenga que estar mirando ese sprite. Outcome-blind igual (marca *que* cruzó un boundary, no *qué tier ganó*). + +**Pin de outcome-blindness (crítico — el cue puede filtrar tier si no se restringe)**: (1) el cue es **idéntico sin importar la dirección** del cruce (subir o bajar de tier suenan igual) — un "sting ascendente vs. thud descendente" sería un signal de valor directo. (2) **No hay cue distintivo en el cruce del D-gate** (`D` 0→1): confirmar Sacrificio salta el readout de 0.10 a un piso garantizado de ≥0.4175, un cruce casi-determinista; un cue especial ahí enseñaría "acabo de asegurar ≥ Tier 2". El D-gate usa el mismo cue genérico de boundary o ninguno. (3) Como solo existen 2 boundaries (0.35, 0.70) y `T` es ~monótona, un jugador no debe poder reconstruir el tier contando cruces — de ahí (1) y (2). Cubierto por AC-H34. + +**A-5 — Ducking del kaiju durante el beat** +A-1 filtra el lecho de combate; queda por decidir explícitamente qué pasa con las **vocalizaciones del kaiju**: opción por defecto propuesta = se duckean con el resto (aísla al héroe, coherente con el Principio C). **Riesgo a resolver (no puramente estético)**: si las vocalizaciones del kaiju cargan telegrafíos de ataque (probable, una vez exista Encuentro con Kaiju) y se duckean durante todo el hold `DYING`, el jugador podría salir de `DEATH_HOLD` a un ataque sin aviso audible. Regla propuesta: los telegrafíos **críticos** del kaiju quedan exentos del ducking de A-1/A-5, o se re-anuncian al liberarse `DEATH_HOLD`. *(Tracked en OQ-10; confirmar con Dirección de Audio y Encuentro con Kaiju cuando existan.)* + +## UI Requirements + +*Referencia rectora: Art Bible Sección 7 (UI 100% diegética, sin capa de HUD estéril — 7.0), 3.3 (gramática de forma de UI), 7.1-7.5. Esta sección resuelve el placeholder de AC-H32 (posición, formato, umbral de visibilidad del readout).* + +**Restricción central (tensión de diseño resuelta):** el readout debe hacer el Test de diseño 1 **legible en tiempo real** (~30s antes de una muerte inevitable, el jugador ve que su decisión cambia el veredicto) **sin narrar** ni convertir la tragedia en una pantalla de min-max. Se resuelve con un indicador **diegético, no numérico y de lectura discreta** anclado al héroe, no un número de HUD. + +**U-1 — Indicador de veredicto proyectado, anclado al elemento asimétrico (multi-canal, NO solo color)** +El readout de `relic_quality` proyectada se ancla al **elemento asimétrico del héroe** (V-1/V-2 — el ancla de la futura reliquia), no es un número ni una barra. **Restricción de accesibilidad (art bible 4.5, regla permanente)**: el par Oro Reliquia vs Hueso/Ocre es el de mayor riesgo de daltonismo del sistema, así que el estado **nunca se señaliza solo por hue/intensidad de dorado**. Se hereda el patrón diegético que el art bible 7.5 ya fijó para la legibilidad de vida: el veredicto se comunica por **tres canales redundantes** — (1) **forma/marco**: un cambio discreto de la geometría del ancla o de su marco por tier (p. ej. filigrana plana → borde parcial → borde de rosetón completo), legible en silueta y bajo daltonismo; (2) **movimiento**: un pulso/bloom cuya presencia (no su color) marca "veredicto activo"; (3) **color** (Oro Reliquia) solo como refuerzo terciario, nunca como único portador. Refuerza el entrenamiento "dorado = reliquia ganada" (art bible 5.1) sin depender del color y sin texto. + +**U-2 — Lectura discreta de 3 estados (mapea a tiers), por forma primero** +El indicador tiene **tres estados legibles** que corresponden a los tiers de la Fórmula H4 — Delgada [0.10,0.35), Sólida [0.35,0.70), Legendaria [0.70,1.00] — diferenciados **primero por forma/marco** (U-1 canal 1), no por rampa continua de color. Esto tiene un beneficio doble: (a) legible de un vistazo a escala RTS y bajo daltonismo; (b) al diferenciar por forma en vez de por gradiente de rareza de color, **evita leerse como el glow de rareza de loot de un ARPG** (Diablo/WoW) — un riesgo real dado el público mid-core/hardcore, que convertiría "veredicto sobre una vida" en "drop de botín". La transición entre estados es discreta y visible, y se acompaña de un **cue de audio/haptic en el cruce de tier** (ver A-4) para no depender de que el jugador esté mirando ese sprite. *Limitación conocida (OQ-8)*: como la mayor influencia táctica cae dentro de una banda de tier, un cambio marginal que no cruza boundary no altera el estado discreto — a evaluar en playtest si hace falta un cue intra-banda sutil (p. ej. tasa de pulso) sin reintroducir el min-max de decimales. + +**U-3 — Umbral de visibilidad: por tiempo-hasta-muerte, no por HP (contrato conjunto con Encuentro con Kaiju)** +El indicador aparece **solo** cuando el héroe está condenado/en riesgo activo **o** está seleccionado. **Corrección clave**: el trigger de "en riesgo" NO puede basarse en HP actual, porque HP% y tiempo-hasta-muerte están desacoplados — un héroe al 90% de vida puede morir en 2s por un burst del kaiju, y ningún umbral de HP fijo garantizaría la ventana de ~30s que el Test de diseño 1 (AC-H29) exige como test duro, no advisory. Por eso el trigger consume una estimación de **`time_to_death`/`incoming_damage_rate`** que debe proveer **Encuentro con Kaiju** (dueño del ritmo del encuentro y de los telegrafíos de ataque): el readout se muestra cuando el tiempo-hasta-muerte proyectado cae bajo la ventana objetivo (~30s). Es un **contrato conjunto**, no un knob de UI en solitario. Un héroe sano y sin muerte inminente no muestra readout — evita ruido de HUD y preserva el understatement. *(El valor concreto de la ventana y su histéresis: OQ-6.)* + +**U-4 — Congelado durante `DYING` (coherencia con el snapshot)** +Al entrar a `DYING`, el indicador **se congela en el valor snapshoteado** (D/T/P del cruce por 0, Fórmula H5 / AC-H15) y deja de actualizarse — durante el beat el Input está en `DEATH_HOLD` y el jugador no puede cambiar nada (AC-H09). El estado congelado es el que se resuelve visualmente en la reliquia (V-6). No hay conflicto entre "readout en vivo" (pre-muerte) y "veredicto sellado" (muerte). + +**U-5 — Sin editorializar (understatement, Principio C / anti-melodrama)** +Prohibido texto de veredicto ("¡Muerte heroica!", "Reliquia legendaria forjada") durante el beat o el readout. El único lenguaje permitido es el diegético del dorado sobre el ancla y su resolución en el ícono de reliquia. La calificación se *muestra*, no se declara (paralelo UI del A-2/A-3 de audio). + +**U-6 — Prioridad de selección legible (Regla Core 3, art bible 7.5)** +La UI de selección debe reflejar la prioridad héroe > tropa: al hacer clic donde se solapan héroe y tropa, el marcador de selección resalta al héroe (AC-H07). El marcador de héroe se diferencia del de tropa por la gramática de forma de UI (7.2/3.3), no solo por tamaño. + +**U-7 — Iconografía de reliquia derivada (art bible 7.2)** +El ícono de la reliquia resultante (post-`DEAD`, consumido por Forja de Legado) sigue el estilo "iluminado" de 7.2 — silueta plana base + pasada de plomo dorado integrado — reconocible como reliquia incluso en silueta/daltonismo, independiente del borde dorado de estado. *(Nota: el ícono final es propiedad de Forja de Legado; este sistema solo provee `relic_category` + `relic_quality` que determinan cuál.)* + +## Acceptance Criteria + +*Convención: cada AC usa formato Dado/Cuando/Entonces y una etiqueta de tipo de evidencia (`[Logic/unit]`, `[Integration]`, `[Visual/Feel]`, `[UI]`) según la taxonomía de evidencia de testing del proyecto. Los AC marcados **(bloqueado)** dependen de un sistema sin GDD aún — hoy solo verificables mediante un mock del contrato en el límite de este sistema; deben re-verificarse contra el GDD real cuando exista.* + +### A. Definición de Héroes y Datos (Reglas Core 1-3, Fórmulas H1-H3) + +**AC-H01 — Carga válida de `HeroDefinition`** **[Logic/unit]** +Dado un `HeroDefinition` con `hero_id`, `hero_name`, `max_health`, `move_speed`, `unit_visual_radius_world`, ≥1 habilidad, `asymmetric_element` y `relic_category` poblados, cuando el sistema carga el `unit_roster` (porción de héroes) de la era ACTIVE, entonces el héroe se instancia sin error y todos los campos son accesibles con los valores exactos del resource. + +**AC-H01b — Validación de stats derivados al cargar** **[Logic/unit]** +Dado un `HeroDefinition` cuyos `max_health`/`move_speed`/`unit_visual_radius_world` autorados NO coinciden con sus fórmulas (H1/H2/H3) a partir de `hero_hp_multiplier`/`pan_speed_fraction`/`sprite_diameter_px` (p. ej. `max_health=50` con multiplicador 3.0 que exige ≥120), cuando el sistema carga el resource, entonces la carga falla de forma ruidosa (error/assert en build de desarrollo) — nunca se usa un stat inconsistente con su fórmula (mismo patrón loud-fail que AC-H02). + +**AC-H02 — Falla ruidosa sin `relic_category`** **[Logic/unit]** +Dado un `HeroDefinition` sin `relic_category` asignado, cuando el sistema intenta cargarlo, entonces la carga falla de forma ruidosa (error/assert en build de desarrollo) y **no** se asigna ninguna categoría por defecto silenciosamente. + +**AC-H03 — `hero_max_health` (H1) exacto por arquetipo** **[Logic/unit]** +Dado `melee_troop_max_health=40` y `hero_hp_multiplier` de 3.5/3.75/5.0, cuando se calcula `hero_max_health`, entonces el resultado es exactamente 140/150/200, y para cualquier `hero_hp_multiplier` en [3.0, 5.0] el resultado es siempre > 40. + +**AC-H04 — `hero_move_speed` (H2) exacto por arquetipo, siempre bajo el techo de tropa** **[Logic/unit]** +Dado `camera_pan_speed=30` y `pan_speed_fraction` de 0.1417/0.1333/0.1167, cuando se calcula `hero_move_speed`, entonces el resultado es exactamente 4.25/4.0/3.5 u/s, y para cualquier `pan_speed_fraction` en [0.10, 0.15] el resultado es siempre < 5.0 u/s (tropa ranged, la más rápida). + +**AC-H05 — `unit_visual_radius_world` (H3) exacto, piso 0.5, techo de tropa** **[Logic/unit]** +Dado `sprite_diameter_px` de 64/72/80 y `world_unit_scale=48`, cuando se calcula `unit_visual_radius_world`, entonces el resultado es exactamente 0.6667/0.75/0.8333, es siempre ≥0.5, y es siempre > 0.4583 (radio máximo de tropa). + +**AC-H06 — `relic_category` es fijo, no emergente** **[Logic/unit]** +Dado un héroe con `relic_category="offensive"`, cuando el héroe muere con cualquier combinación de D/T/P, entonces `relic_category` expuesto a Permadeath sigue siendo `"offensive"` — solo `relic_quality` varía con la circunstancia de muerte. + +**AC-H07 — Prioridad de selección héroe > tropa** **[Integration]** +Dado un héroe y una o más tropas cuyos radios de selección se solapan en el punto de clic, cuando el jugador hace clic, entonces el héroe es seleccionado; si hay múltiples héroes solapados, se selecciona el más cercano al punto de clic. + +### B. Estados y Transiciones + +**AC-H08 — Umbral de muerte (H5)** **[Logic/unit]** +Dado un héroe en cualquier estado `ALIVE_*` con `health_current > 0`, cuando `health_current` llega a ≤0, entonces el héroe transiciona a `DYING` en ese mismo frame, sin importar el estado `ALIVE_*` de origen. + +**AC-H09 — `DYING` activa `DEATH_HOLD`** **[Integration]** +Dado un héroe que transiciona a `DYING`, cuando la transición ocurre, entonces el contexto de Input pasa a `DEATH_HOLD` y toda entrada del jugador queda suprimida hasta que el beat se libere. + +**AC-H10 — Héroe seleccionado que muere se remueve de la selección** **[Integration]** +Dado un héroe actualmente en el conjunto de selección activo, cuando transiciona a `DYING`, entonces se remueve inmediatamente de ese conjunto de selección. + +**AC-H11 — `DYING → DEAD` solo tras liberar el beat** **(bloqueado — timing propiedad de Permadeath, sin GDD)** **[Integration]** +*Mock Contract Assumptions: el mock de Permadeath asume un único hook síncrono `release_beat()` que dispara la transición. Si el GDD real usa un modelo distinto (callback asíncrono, señal con payload), re-verificar.* +Dado un héroe en `DYING`, cuando el beat sostenido (controlado por Permadeath) se libera, entonces el héroe transiciona a `DEAD`; antes de esa liberación, el héroe permanece en `DYING` sin importar cuánto tiempo pase. Verificable hoy con un mock de Permadeath que controla el timing del beat. + +**AC-H12 — Contrato de exposición completo en `DYING→DEAD`** **[Integration]** +Dado un héroe que transiciona de `DYING` a `DEAD`, cuando la transición se confirma, entonces el sistema expone a Permadeath (o a su mock) exactamente `hero_id`, `hero_name`, `relic_category`, `relic_quality` (evaluada vía Fórmula H4 en el frame del snapshot de `DYING`, nunca recalculada después) y el payload D/T/P snapshoteado — sin campos faltantes ni nulos. *(Cierra el contrato que Permadeath asume en su Regla 1/AC-P01/AC-P02 — ver `design/gdd/permadeath.md` OQ-P6 #1.)* + +### C. Fórmula `relic_quality` (H4) — el corazón del Marco B + +**AC-H13 — Test negativo de agencia: piso incondicional** **[Logic/unit]** +Dado `D=0, T=0.95, P=1` (estaca y propósito máximos, pero sin intención), cuando se calcula `relic_quality`, entonces el resultado es exactamente 0.10 (Tier 1 Delgada) — T y P no tienen ningún efecto sin `D=1`. + +**AC-H14 — Test negativo de agencia: la compuerta se abre con D=1** **[Logic/unit]** +Dado las mismas circunstancias que AC-H13 pero con `D=1`, cuando se calcula `relic_quality`, entonces el resultado es estrictamente mayor a 0.10 — confirmando que existe un estado "desperdiciado" alcanzable (AC-H13) y que la intención, no la escena, es la compuerta. + +**AC-H15 — Snapshot D/T/P congelado, no releído** **[Logic/unit]** +Dado un héroe cuyo `health_current` cruza 0 con `D=1, T=0.30, P=0` en ese frame, cuando `T` y/o `P` cambian en frames posteriores (antes de que se confirme `DEAD`), entonces el `relic_quality` calculado usa los valores del frame de cruce, no los valores posteriores. + +**AC-H16 — Ejemplo canónico 1: Desperdiciada** **[Logic/unit]** +Dado `D=0`, cuando se calcula `relic_quality`, entonces el resultado es exactamente 0.10 → Tier 1 Delgada. + +**AC-H17 — Ejemplo canónico 2: Deliberada temprana** **[Logic/unit]** +Dado `D=1, T=0.05, P=0`, cuando se calcula `relic_quality`, entonces el resultado es exactamente 0.4175 → Tier 2 Sólida. + +**AC-H18 — Ejemplo canónico 3: Bien gastada, climática** **[Logic/unit]** +Dado `D=1, T=0.95, P=1`, cuando se calcula `relic_quality`, entonces el resultado es exactamente 0.9825 → Tier 3 Legendaria. + +**AC-H19 — Boundary de tier inferior (0.35)** **[Logic/unit]** +Dado `relic_quality=0.35` exacto, cuando se mapea a tier, entonces el resultado es Tier 2 Sólida (límite inclusivo); dado `relic_quality=0.3499`, entonces el resultado es Tier 1 Delgada. + +**AC-H20 — Boundary de tier superior (0.70)** **[Logic/unit]** +Dado `relic_quality=0.70` exacto, cuando se mapea a tier, entonces el resultado es Tier 3 Legendaria (límite inclusivo); dado `relic_quality=0.6999`, entonces el resultado es Tier 2 Sólida. + +**AC-H21 — `D` requiere comando Sacrificio activo + muerte dentro de `sacrifice_radius`** **(bloqueado — clasificación provista por Combate/Daño, sin GDD; ver OQ-2)** **[Logic/unit]** +*Mock Contract Assumptions: el mock de Combate/Daño asume que expone (a) un flag `sacrifice_command_active` por héroe, (b) el destino ordenado del comando, y (c) que el confirm-step ya se resolvió antes de marcar el comando como activo. Re-verificar contra el GDD real de Combate/Daño.* +Dado que el jugador emitió y confirmó un comando Sacrificio con destino X sobre un héroe, cuando el héroe muere con el comando aún activo y dentro de `sacrifice_radius` de X, entonces `D=1`; si muere fuera de `sacrifice_radius` de X, entonces `D=0`. + +**AC-H21b — Knockback dentro del radio preserva `D=1`** **(bloqueado — ver OQ-2)** **[Logic/unit]** +*Mock Contract Assumptions: igual que AC-H21.* +Dado un héroe con comando Sacrificio activo hacia X, cuando un knockback lo desplaza del punto X exacto pero muere dentro de `sacrifice_radius` de X, entonces `D=1` — la mala suerte física no penaliza un sacrificio genuino. + +**AC-H21c — Comando cancelado o sin confirmar → `D=0`** **(bloqueado — ver OQ-2)** **[Logic/unit]** +*Mock Contract Assumptions: igual que AC-H21; el mock asume que un tap sin confirmación nunca marca `sacrifice_command_active=true`.* +Dado (i) un héroe cuyo comando Sacrificio se canceló/reemplazó antes del frame de muerte, o (ii) un héroe sobre el que se tocó Sacrificio sin completar el confirm-step, cuando el héroe muere, entonces `D=0` en ambos casos. + +**AC-H22 — Overkill no afecta `relic_quality`** **[Logic/unit]** +Dado dos escenarios idénticos en D/T/P donde uno recibe un golpe de overkill (vida muy por debajo de 0, clamped) y el otro no, cuando se calcula `relic_quality` en ambos, entonces el resultado es idéntico en ambos casos. + +**AC-H23 — Loud-fail de tuning knobs individuales fuera de rango al cargar** **[Logic/unit]** +Dado un valor de configuración fuera de su rango seguro individual (p. ej. `base_accidental=0.20`, rango seguro 0.05–0.15; o `base_deliberate=0.60`, rango 0.35–0.50; o `sprite_diameter_px=40`, rango 48–96), cuando el sistema carga la configuración, entonces la carga **falla ruidosamente** (error/assert en build de desarrollo) y el valor **no** se clampa ni se sustituye silenciosamente — nunca se ejecuta un cálculo de `relic_quality` (ni de H1/H3) con un knob fuera de rango. (Reemplaza el comportamiento de clamp anterior; coherente con AC-H02.) + +**AC-H23b — Piso de suma conjunta: Tier 3 alcanzable** **[Logic/unit]** +Dado que los pisos individuales (base_deliberate≥0.35, w_stakes≥0.20, w_purpose≥0.15) suman exactamente 0.70, la suma conjunta **no puede** caer por debajo de 0.70 sin que algún knob viole su piso individual (ya atrapado por AC-H23). El check de suma-floor `< 0.70` se conserva como defensa-en-profundidad (belt-and-suspenders) y como guard si un rango individual se afloja en el futuro: cuando la suma de knobs individualmente válidos es `< 0.70`, la carga **falla ruidosamente**. Verificación de reachability: con la suma = 0.70 exacto (todos en su piso), `relic_quality(D=1,T=1,P=1) = 0.70` mapea a Tier 3 Legendaria (límite inclusivo, AC-H20) — Tier 3 es alcanzable en el mínimo conjunto. + +**AC-H23c — Techo de suma conjunta: Legendaria sigue exigente** **[Logic/unit]** +Dado un config donde `base_deliberate + w_stakes + w_purpose > 1.00` (p. ej. 0.50+0.40+0.30 = 1.20, cada término en rango individual), cuando el sistema carga la configuración, entonces la carga **falla ruidosamente** — nunca se usa un config donde Legendaria se alcance con `T=0` (muerte en calma). + +**AC-H23d — Piso de `base_deliberate`: una muerte deliberada nunca colisiona con una accidental** **[Logic/unit]** +Dado un config con `base_deliberate < 0.35` (p. ej. 0.30, que satisface la suma conjunta 0.30+0.25+0.15 = 0.70 pero viola el floor individual), cuando el sistema carga la configuración, entonces la carga **falla ruidosamente** — porque `relic_quality(D=1, T=0, P=0) = base_deliberate < 0.35` caería en Tier 1 Delgada, colisionando con una muerte accidental (`D=0` → `base_accidental`). Verificación complementaria: con `base_deliberate = 0.35` exacto y `T=0,P=0`, `relic_quality(D=1) = 0.35` → Tier 2 Sólida (límite inclusivo, AC-H19), estrictamente por encima de cualquier resultado accidental. + +### D. Edge Cases de Concurrencia, Overkill y Persistencia + +**AC-H24 — Snapshots independientes en muerte simultánea** **[Integration]** +Dado dos héroes cuyo `health_current` cruza 0 en el mismo frame con circunstancias D/T/P distintas entre sí, cuando ambos transicionan a `DYING`, entonces cada uno snapshotea su propio payload independientemente, sin que uno sobreescriba o promedie con el otro. + +**AC-H25 — Serialización de beats de muerte simultáneos** **(bloqueado — propiedad de Permadeath, sin GDD)** **[Integration]** +*Mock Contract Assumptions: el mock de Permadeath asume serialización FIFO (orden de llegada). Si el GDD real usa prioridad (p. ej. por relic_quality o por hero_id), re-verificar el orden esperado.* +Dado dos héroes en `DYING` en el mismo frame, cuando Permadeath procesa las muertes, entonces no hay dos `DEATH_HOLD` sostenidos simultáneos — se resuelven en secuencia. Verificable hoy solo con un mock de Permadeath que serialice las llamadas entrantes. + +**AC-H26 — Autosave obligatorio en muerte de héroe, con payload completo marcado como pendiente de sellar** **[Integration]** +Dado un héroe que transiciona a `DYING`, cuando la transición ocurre, entonces Guardado/Persistencia recibe un disparo de autosave obligatorio antes de que el flujo continúe a la siguiente pantalla/estado; el snapshot persistido incluye el payload de muerte completo (`hero_id`, `hero_name`, `relic_category`, `relic_quality` ya computada, D/T/P) marcado explícitamente como *muerte pendiente de sellar* — no solo `state=DYING` sin datos. *(Contrato con Permadeath Regla 6 — cierra la ventana de crash de la que AC-H27/AC-H27c dependen; ver `design/gdd/permadeath.md` OQ-P6 #2.)* + +**AC-H27 — Irreversibilidad a través de crash** **[Integration]** +Dado un héroe que cruzó `health_current ≤ 0` y el autosave obligatorio ya se disparó, cuando el juego se cierra abruptamente (crash) durante el beat `DYING` y se reinicia, entonces el héroe carga como `DEAD` — nunca como `ALIVE` con vida recargada. + +**AC-H27c — Coacción `DYING → DEAD` con reliquia forjada al recargar (contrato con Permadeath Regla 8b / AC-P18-ext)** **[Integration]** +Dado un héroe persistido en `DYING` cuyo autosave incluye el payload completo de muerte marcado como *muerte pendiente de sellar* (AC-H26), cuando el flujo de carga se ejecuta tras un crash o cierre ocurrido en cualquier punto del beat `HOLDING` de Permadeath, entonces el héroe se coacciona a `DEAD` (nunca permanece en `DYING` ni carga como `ALIVE` con vida recargada) y su reliquia se forja desde la `relic_quality` persistida — sin reproducir el beat visual `DEATH_HOLD` (el beat es una experiencia de sesión, no un estado a restaurar). Esta coacción la posee el flujo de carga de Héroes + Guardado/Persistencia; Permadeath no participa — arranca con su cola vacía tras cualquier reload (ver Permadeath AC-P18). + +**AC-H27b — Un héroe `DEAD` es inaccionable en sesión (Regla 4, estado terminal)** **[Logic/unit]** +Dado un héroe en estado `DEAD`, cuando se le intenta emitir cualquier orden (movimiento, Sacrificio), aplicar un efecto de curación, o agregarlo al conjunto de selección, entonces toda acción es rechazada/no-op y el héroe permanece `DEAD` — confirma que `DEAD` es terminal sin transiciones salientes (tabla de estados), no solo a través del path de crash de AC-H27. + +**AC-H28 — Este sistema no declara victoria/derrota** **[Logic/unit]** +Dado que todos los héroes de la era mueren, cuando la última muerte se procesa, entonces el Sistema de Héroes no emite ninguna señal ni llamada de evaluación de victoria/derrota — solo notifica cada muerte individual a sus consumidores declarados. + +### E. Tests de Diseño y Validación de Playtest (Player Fantasy) + +**AC-H29 — Ventana de decisión de ~30s (Test de diseño 1)** **(contrato de datos resuelto — Encuentro con Kaiju Approved; queda como Integration sobre era jugable + verbo Sacrificio de Control)** **[Integration]** +*Contrato real (ya no mock, lado Encuentro): Encuentro con Kaiju (Approved) provee `time_to_death` por héroe (F2 = `telegraph_duration_s + kill_time_hits×attack_cooldown_s`, proyección pre-contacto que da 32.0s/36.5s para los arquetipos MVP a HP completo) **e** `incoming_damage_rate_total` (F6, DPS combinado kaiju+esbirros); el readout (U-3) debe reflejar la fuente más peligrosa (Encuentro Regla 6b/AC-K68). Los cross-checks del lado receptor son Encuentro AC-K26/AC-K68. Sigue siendo Integration porque necesita (a) una era jugable y (b) el verbo Sacrificio de Control (aún no escrito, OQ-2) — pero el `time_to_death` ya no es un mock.* +Dado un héroe cuya muerte es inevitable, cuando se observan los ~30 segundos previos a la muerte (proyectados por el `time_to_death` real de Encuentro con Kaiju), entonces existió al menos una decisión táctica disponible al jugador (comando Sacrificio, reposicionamiento) que habría cambiado el D/T/P resultante snapshoteado, y el readout de veredicto fue visible durante esa ventana. + +**AC-H30 — Estado "desperdiciado" alcanzable en juego real (Test de diseño 2)** **(contratos de diseño Approved — Combate/Daño + Encuentro con Kaiju; requiere una era jugable implementada)** **[Integration]** +*Nota: los contratos de diseño de los que depende (Combate/Daño y Encuentro con Kaiju) ya están **Approved** — ya no es un hueco de diseño. Sigue siendo un Integration test que requiere una era jugable **implementada** (instanciar un héroe, infligirle daño letal sin comando Sacrificio activo, y leer el `relic_quality` resultante). Se ejecuta en el vertical slice, no contra un mock.* +Dado una era jugable completa, cuando se juega sin emitir ningún comando Sacrificio sobre un héroe que termina muriendo, entonces el resultado es `D=0` → Tier 1 Delgada — confirmando en contexto real (no solo en unit test aislado, AC-H13/AC-H16) que ninguna regla oculta fuerza `D=1`. + +**AC-H31 — Validación de playtest: lectura espontánea de la muerte** **[Integration]** +Dado una sesión de playtest documentada tras la muerte de un héroe, cuando se le pide al playtester describir la muerte sin preguntas dirigidas, entonces la transcripción se registra en `production/qa/evidence/` y **qa-tester la codifica contra una rúbrica pre-aprobada por game-designer** que distingue tres registros: (a) trágico-atribuido (nombra al héroe y atribuye el resultado a su acción específica), (b) transaccional-frío (lenguaje de descripción de ítem: "conseguí un Tier 3"), (c) solo-afecto (solo tristeza/frustración, sin mencionar qué compró la muerte). El Marco B se considera validado solo si predomina (a) por sobre (b) y (c). Evidencia cualitativa documentada — no automatizable; requiere ≥1 sesión por era del MVP. *(La rúbrica de 3 registros evita el falso positivo de "mencionó qué compró antes que cómo se sintió" que no distingue tragedia de transaccionalidad frío — ver Player Fantasy.)* + +**AC-H31b — El D-gate no deriva a "culpa pura" (validación de feel del confirm-step)** **(playtest — vertical slice; depende del comando Sacrificio real de Combate/Daño)** **[Integration]** +Dado ≥1 sesión de playtest documentada por era del MVP en la que un héroe muere con `D=0` tras un intento **genuino** de sacrificio del jugador (comando Sacrificio emitido pero muerte fuera de `sacrifice_radius`, o el jugador holdeó una línea climática con órdenes ordinarias sin emitir Sacrificio), cuando `qa-tester` codifica la reacción del playtester, entonces se registra si el registro predominante es "veredicto entendido/justo" vs "castigo/gotcha injusto" (culpa pura, ver Player Fantasy §criterio de validación). Si predomina "culpa pura" en ≥[umbral a fijar en QA plan] de los casos, se **activa la alternativa registrada en OQ-11** (ampliar `D=1` a Hold sostenido contra probabilidad letal). Esta AC existe porque la re-review (game-designer + narrative-director, convergentes) señaló que el confirm-step puede sobre-restringir "deliberado" a un solo verbo de menú; el playtest con controlador en mano es el árbitro, no otra ronda de rediseño en papel. + +### F. UI Readout + +**AC-H32a — Transición discreta de estado en boundaries de tier** **[UI]** +Dado un héroe condenado con readout de veredicto visible, cuando `relic_quality` proyectado cruza un boundary de tier (0.35 o 0.70), entonces el estado visual discreto del readout cambia exactamente en ese boundary (mapeo idéntico a AC-H19/AC-H20), y no se observa ningún estado intermedio/continuo entre los 3 estados. (Ver U-2.) + +**AC-H32b — Umbral de visibilidad del readout** **(contrato de datos resuelto — Encuentro con Kaiju Approved provee `time_to_death`; el valor de umbral concreto sigue abierto en OQ-6)** **[UI]** +*Contrato real (ya no mock): Encuentro con Kaiju (Approved) expone `time_to_death`/`incoming_damage_rate_total` por héroe (F2/F6), y el readout se muestra cuando la proyección de la fuente más peligrosa cae bajo la ventana objetivo. La regla binaria (oculto vs visible) es verificable contra el contrato real. El **valor de umbral concreto** (~30s) y su histéresis quedan abiertos (OQ-6, a afinar en `/ux-design` — ver también Encuentro OQ-14 sobre anti-parpadeo de la señal dual).* +Dado un héroe sano y no seleccionado, cuando se observa la pantalla, entonces el readout está oculto; dado un héroe marcado en-riesgo (por el trigger de time-to-death) **o** seleccionado, entonces el readout es visible. (Ver U-3.) + +**AC-H32c — Freeze del readout en `DYING`** **[Integration]** +Dado un héroe que entra a `DYING`, cuando ocurre la transición, entonces el readout se congela en el valor snapshoteado (AC-H15) y no vuelve a actualizarse hasta resolverse en el ícono de reliquia (V-6). (Ver U-4.) + +**AC-H32d — Sin texto editorializante durante readout/beat** **[UI]** +Dado que el readout de veredicto o el beat de muerte está activo, cuando se observa la pantalla, entonces no se renderiza ningún string de texto de veredicto ("¡Muerte heroica!", "Reliquia legendaria forjada", etc.). Evidencia: walkthrough manual (understatement, U-5 / A-2). (Ver U-5.) + +**AC-H32e — Estado de tier distinguible sin color (regla permanente de daltonismo, art bible 4.5)** **[UI]** +Dado el readout de veredicto visible en cualquiera de sus 3 estados de tier, cuando se observa bajo simulación de daltonismo (deuteranopia/protanopia) **o** en escala de grises (canal de color removido), entonces los 3 estados siguen siendo distinguibles entre sí por forma/marco (U-1 canal 1) y por la presencia/ausencia de movimiento — el canal de color (Oro Reliquia) **nunca** es el único diferenciador. Evidencia: walkthrough manual con filtro de daltonismo/grayscale a escala Nivel 1 LOD (24–32px). Cubre la "regla permanente" de U-1 que AC-H32a (solo transición discreta) no verifica. (Ver U-1, art bible 4.5.) + +**AC-H33 — Marcador de selección diferencia héroe de tropa** **[UI]** +Dado un héroe y una tropa ambos seleccionables, cuando cada uno se selecciona, entonces el marcador de selección del héroe se distingue visualmente del de la tropa por la gramática de forma de UI (art bible 7.2/3.3), no solo por tamaño — cubre U-6, que AC-H07 (solo lógica de prioridad) no verifica. + +### G. Audio (contrato provisional — re-verificar con Dirección de Audio) + +*Los requisitos de audio A-1..A-5 son un contrato provisional sin audio bible aún; solo A-4 tiene un trigger discreto testeable hoy. A-1/A-2/A-3/A-5 son cualitativos/de dirección y su evidencia (walkthrough + sign-off de audio-director) se define cuando exista Dirección de Audio — ver Open Questions.* + +**AC-H34 — El cue de la ventana de decisión (A-4) es outcome-blind** **(bloqueado — timing/assets de audio dependen de Dirección de Audio, sin GDD)** **[Integration]** +*Mock Contract Assumptions: el mock dispara el cue en cada cruce de boundary (0.35, 0.70) del readout proyectado y en el cruce del D-gate, exponiendo qué muestra de audio se reproduce.* +Dado un readout de veredicto en vivo durante la ventana de decisión, cuando `relic_quality` proyectado cruza un boundary de tier hacia arriba, hacia abajo, **o** cruza el D-gate (`D` 0→1), entonces el cue reproducido es **idéntico en los tres casos** (misma muestra, sin variante direccional ni sting especial en el D-gate) — de modo que el jugador no puede inferir *qué tier* alcanzó ni *en qué dirección* se movió, solo *que* algo cambió. (Ver A-4.) + +## Open Questions + +*Preguntas abiertas y contratos provisionales que este GDD deja registrados para resolver cuando existan los sistemas dependientes. Ninguna bloquea la validez del diseño actual, pero varias bloquean la *verificación* de AC específicas (ver Acceptance Criteria).* + +**OQ-1 — ¿Quién posee la evaluación de victoria/derrota de era?** +Cuando todos los héroes de una era mueren es (casi con certeza) la condición de derrota, pero este sistema **no** posee esa evaluación (AC-H28) — solo notifica cada muerte. Dueño candidato: **Encuentro con Kaiju** o **Transición de Era**. Mismo hueco abierto que en Sistema de Tropas. *Resolver al diseñar esos sistemas.* + +**OQ-2 — Contrato de clasificación `D` (deliberada) y `P` (propósito)** +La Fórmula H4 consume `D` (comando Sacrificio activo + muerte dentro de `sacrifice_radius`, ver definición completa en H4) y `P` (murió en `guard_radius` de un objetivo marcado **o** neutralizando una amenaza marcada del kaiju). Ambas clasificaciones las debe proveer **Combate/Daño** (sin GDD aún). Los *thresholds* ya no están indefinidos — son los tuning knobs `sacrifice_radius` (default 3.0) y `guard_radius` (default 4.0); lo pendiente es que Combate/Daño **implemente y confirme** la clasificación (incluido el confirm-step del comando Sacrificio y la vía ofensiva de `P`), no inventar el umbral. Bloquea la verificación real de AC-H21/H21b/H21c y de las AC de `relic_quality` que dependen de `D`/`P` de gameplay (hoy testeables con mock). *Resolver al diseñar Combate/Daño y el sistema de órdenes.* + +**OQ-3 — Provisión de `kaiju_timer_remaining_ratio` (input `T`)** — ✅ **RESUELTO (2026-08-07)** +`T = 1 − kaiju_timer_remaining_ratio`. El ratio lo **posee el Temporizador de Preparación/Ritual** (Regla 3 / Fórmula F2 de ese GDD, ya diseñado): va de 1.0 al inicio de la Preparación a 0.0 en el clímax, fijado en 0 durante el clímax. Este sistema lo lee en el frame de muerte para `T`; Encuentro con Kaiju lo consume en paralelo (no lo posee). *Nota: AC-H29 (ventana de decisión de ~30s) sigue bloqueada, pero por una razón más acotada — depende del `time_to_death` por héroe que provee Encuentro con Kaiju (U-3), no del ratio global, que ya tiene dueño.* + +**OQ-4 — Contrato crítico con Permadeath (dueño del beat de muerte)** — *(parcialmente resuelto 2026-08-15: Permadeath ya tiene GDD — `design/gdd/permadeath.md`. El payload ahora incluye `relic_quality` (ver AC-H12 actualizado); AC-H11/AC-H25 siguen bloqueados porque su verificación end-to-end requiere Permadeath implementado, no solo diseñado.)* +Permadeath (**consumidor crítico**) recibe el payload de circunstancia (`hero_id`, `hero_name`, `relic_category`, `relic_quality`, D/T/P snapshoteados) y controla el *timing* del beat `DEATH_HOLD` y la confirmación de `DEAD`. Bloquea la verificación de AC-H11 y AC-H25 (hoy testeables con mock, pendientes de Permadeath implementado). Es el contrato más importante del proyecto: define cómo la muerte se convierte en legado. + +**OQ-5 — ¿Lockear los 3 arquetipos como héroes reales del MVP?** +Vanguardia / Portaestandarte / Guardián se usan a lo largo del GDD como **ilustraciones** de las 3 `relic_category`, no como un roster cerrado. Decisión pendiente: ¿se vuelven los héroes reales de la primera era del MVP, o quedan solo como plantillas y el roster real se define en Datos de Era/Civilización? *Decidir al poblar el roster de la primera era.* + +**OQ-6 — Ventana concreta de `time_to_death` para el readout de UI (U-3)** +El trigger de visibilidad del readout ya está resuelto en *tipo* (por `time_to_death` proyectado, contrato conjunto con Encuentro con Kaiju — U-3), no por HP. Lo que queda abierto es el **valor concreto de la ventana** (~30s objetivo) y su histéresis (para que no parpadee cuando el time-to-death oscila cerca del umbral). *Afinar en Pre-Producción / vertical slice, junto con Encuentro con Kaiju.* + +**OQ-7 — ¿Pesos de H4 relativos al arquetipo? (alternativa a la vía dual de `P`)** +La homogeneización de arquetipos (ofensivos castigados con `P=0`) se resolvió dando a `P` una vía ofensiva (ver H4). Alternativa no elegida, a revisitar si el playtest muestra que la vía dual no basta: hacer `w_stakes`/`w_purpose` relativos al arquetipo. Se prefirió la vía dual por simplicidad de tuning. *Revisitar en playtest de balance.* + +**OQ-8 — ¿Cue intra-banda para decisiones que no cruzan boundary de tier?** +El readout discreto de 3 estados (U-2) no cambia cuando una decisión mejora `relic_quality` sin cruzar un boundary de tier (p. ej. subir `T` dentro de la banda Sólida). A evaluar en playtest si eso hace sentir que "no pasó nada" y si hace falta un cue intra-banda sutil (tasa de pulso, etc.) sin reintroducir el min-max de decimales que U-2 evita a propósito. *Afinar en playtest.* + +**OQ-9 — Estructura de `AbilityDefinition`** +`HeroDefinition.abilities` es `Array[AbilityDefinition]` con ≥1 entrada, pero la estructura interna de `AbilityDefinition` (tipo de habilidad, coste, cooldown, target, efecto) es un contrato del futuro sistema de Habilidades/Combate, sin GDD aún. Este sistema solo exige que el array exista y no esté vacío (AC-H01). *Definir al diseñar Combate/Habilidades.* + +**OQ-10 — Exención de telegrafíos del kaiju durante el ducking del beat (A-5)** +Durante el hold `DYING`, A-1/A-5 duckean el lecho de combate y (por defecto propuesto) las vocalizaciones del kaiju. Si esas vocalizaciones cargan telegrafíos de ataque, duckearlas por completo podría hacer que el jugador salga de `DEATH_HOLD` a un ataque sin aviso audible. Regla propuesta: telegrafíos críticos exentos del ducking, o re-anunciados al liberarse el beat. *Resolver con Dirección de Audio y Encuentro con Kaiju (owners del telegrafío) cuando existan.* + +**OQ-11 — Alternativa registrada para el D-gate (si el confirm-step deriva a "culpa pura")** +El D-gate actual exige un comando **Sacrificio** dedicado con confirm-step (Regla 5, H4). La re-review de 2026-08-07 (game-designer + narrative-director, convergentes) señaló un riesgo real: el confirm-step puede leerse como "ritual-as-checkbox" y hacer que una muerte holdeando una línea climática con órdenes ordinarias (Hold/Attack-Move) marque `D=0` → Tier 1, contradiciendo la fantasía "El Que Decide Cómo Termina" y potencialmente derivando al estado de falla que el propio Player Fantasy define ("frustración/castigo → culpa pura"). **Decisión consciente (creative-director)**: para el MVP se mantiene el confirm-step (es la solución anti-exploit correcta e implementable que resolvió un bloqueante previo); la preocupación de *feel* es una hipótesis de playtest, no un defecto de spec, y este es el segundo ciclo sobre la misma mecánica — se valida con controlador en mano, no con otra ronda de rediseño en papel. **Alternativa registrada (NO adoptada; se activa si AC-H31b muestra "culpa pura" predominante)**: ampliar `D=1` para que también se dispare con una orden Hold/engage **sostenida contra probabilidad letal** (order-intent-persistence: duración mínima + sin orden de retirada activa), manteniendo el comando Sacrificio como el ritual de mayor techo/opcional — esto restaura la agencia de tácticas orgánicas sin reabrir el exploit del Hold reflejo (duración + no-retirada sigue siendo un compromiso real). **Gap asociado**: la forma de input concreta del confirm-step (hold-duration / doble-clic / modal) no está especificada y debe caber en la ventana de reacción de ~30s — contrato con Combate/Daño. *Resolver en vertical slice.* diff --git a/design/gdd/sistema-de-tropas.md b/design/gdd/sistema-de-tropas.md new file mode 100644 index 0000000000..ffac8dd69a --- /dev/null +++ b/design/gdd/sistema-de-tropas.md @@ -0,0 +1,309 @@ +# Sistema de Tropas + +> **Status**: In Design +> **Author**: usuario + agentes +> **Last Updated**: 2026-07-23 +> **Implements Pillar**: Sacrificio con Peso (Pilar 1); Preparación Ritual, Clímax Explosivo (Pilar 3) + +## Overview + +El Sistema de Tropas define y gobierna las unidades rasas del jugador: soldados uniformes que se instancian desde el `unit_roster` de la era activa (leído de Datos de Era/Civilización), se mueven por el mapa cuando reciben órdenes, y forman la masa numérica del ejército. A nivel de datos, cada tipo de tropa es una definición tipada con stats básicos (vida, velocidad, footprint de un tile) y un `unit_visual_radius_world` que el sistema de Control y Selección consume para resolver clics. A nivel de jugador, las tropas cumplen un rol emocional peculiar y deliberado: son **prescindibles por diseño**. Su uniformidad visual y su bajo costo individual (art bible Sección 5.1) existen precisamente para que gastarlas no genere fricción — y ese contraste es lo que hace que la muerte de un héroe *sí* pese (Pilar 1). Sin las tropas, no habría una "masa" contra la cual medir el valor único de un héroe; son el telón mortal sobre el que se recorta la tragedia. + +## Player Fantasy + +La fantasía de las tropas es la del **peso de los números** — la sensación de comandar una hueste, de ver una masa de soldados avanzar y chocar contra fuerzas mayores. Pero es una fantasía deliberadamente incompleta, y esa incompletitud es el punto. A diferencia de un RTS tradicional donde cada unidad puede llegar a importar, aquí las tropas están diseñadas para que el jugador *no* se encariñe con ninguna en particular. Son intercambiables, uniformes, sin nombre. El jugador las gasta como recurso, no como individuos. Esta frialdad calculada es lo que da forma al contraste emocional del juego (Pilar 1): cuando el jugador ha pasado horas gastando tropas sin remordimiento, la primera vez que un *héroe* —una unidad con nombre, silueta única y arco propio— está en peligro, la diferencia se siente en el cuerpo. Las tropas enseñan al jugador, a través de la repetición desapegada, qué se siente cuando algo *sí* importa. Su fantasía no es que las ames; es que su abundancia haga insoportable la escasez de lo que viene después. + +## Detailed Design + +### Core Rules + +1. **Tipos de tropa data-driven**: cada tipo de tropa es una `Resource` tipada (`TroopDefinition`) con campos: `troop_id`, `troop_name`, `max_health`, `move_speed` (world units/sec), `unit_visual_radius_world` (consumido por Control y Selección, fórmula `select_radius`), `sprite_footprint` (1 tile por regla), y un rol táctico (`melee` / `ranged` / `support`). El MVP soporta **2-3 tipos** por civilización, leídos del `unit_roster` de la era ACTIVE. +2. **Aprovisionamiento por dotación fija + refuerzos limitados**: el jugador comienza cada era con un ejército definido por la `EraDefinition`. Durante la fase de **Preparación** (antes de que el temporizador del kaiju inicie), puede reclutar refuerzos hasta un tope (`max_reinforcements`). No hay construcción de base ni producción continua — coincide con el anti-pilar "no construcción de base masiva". +3. **Uniformidad deliberada**: todas las tropas del mismo tipo son mecánicamente idénticas — mismos stats, sin individualización, sin nombres, sin progresión individual. Esto es intencional (Pilar 1, art bible Sección 5.1): la masa debe sentirse intercambiable. +4. **Movimiento por orden**: las tropas se mueven solo cuando reciben una orden de Control y Selección (`move_commanded` resuelto a un destino de mundo). Cada tropa resuelve su propio pathfinding hacia el destino (vía `NavigationAgent2D`); no hay formación impuesta en el MVP — llegan al área del destino de forma orgánica. +5. **Prioridad de selección por debajo de héroes**: las tropas siempre ceden prioridad de selección a los héroes cercanos (regla ya fijada en Control y Selección) — un clic ambiguo nunca selecciona una tropa si hay un héroe en el radio. +6. **Muerte no permanente-narrativa**: la muerte de una tropa es definitiva dentro de una era (no reaparece), pero a diferencia de los héroes, **no dispara permadeath ni forja de legado** — una tropa muerta es solo una baja, no una tragedia. Esto es lo que las distingue mecánicamente de los héroes y protege el peso del Pilar 1. + +### States and Transitions + +| Estado | Descripción | Transición desde | Transición hacia | +|---|---|---|---| +| `IDLE` | En el campo, sin orden activa, en posición | Instanciación, llegada a destino | `MOVING`, `ENGAGED`, `DEAD` | +| `MOVING` | Ejecutando pathfinding hacia un destino ordenado | Orden de movimiento | `IDLE` (llega), `ENGAGED` (entra en combate), `DEAD` | +| `ENGAGED` | En combate (delegado a Combate/Daño) | Contacto con enemigo, orden de ataque | `IDLE` (enemigo muere/se retira), `MOVING` (orden de reposicionar), `DEAD` | +| `DEAD` | Vida ≤ 0, removida del campo | Cualquier estado al llegar vida a 0 | — (terminal; la selección se limpia si estaba seleccionada) | + +- La resolución real del combate (cálculo de daño) pertenece a Combate/Daño; este sistema solo mantiene el estado y la vida de la tropa, y reacciona a los eventos de daño. + +### Interactions with Other Systems + +| Sistema | Dirección | Interfaz | +|---|---|---| +| Datos de Era/Civilización | Datos → este sistema | Lee `unit_roster` (porción de tropas) de la era ACTIVE para instanciar tipos | +| Control y Selección de Unidades | bidireccional | Provee posición/hitbox y `unit_visual_radius_world` para resolución de selección; recibe órdenes de movimiento para las tropas seleccionadas | +| Combate/Daño | bidireccional | Delega el cálculo de daño; recibe eventos de daño que reducen la vida de la tropa; notifica el estado `ENGAGED`/`DEAD` | +| Encuentro con Kaiju | este sistema → Kaiju | Las tropas son objetivos válidos para los ataques del kaiju/esbirros; expone posiciones para la IA del kaiju | +| Guardado/Persistencia | este sistema → Guardado | El conteo/estado de tropas vivas es parte del estado de era guardable (aunque las tropas individuales no son entidades de legado) | + +*(Nota provisional: Combate/Daño y Encuentro con Kaiju aún no tienen GDD — esas interfaces son contratos propuestos. Datos de Era/Civilización, Control y Selección y Guardado/Persistencia SÍ están diseñados y sus contratos son confirmados.)* + +## Formulas + +### Stats base por tipo de tropa (MVP, tuneables) + +Tres arquetipos, diferenciados solo por los tres campos que esta GDD posee (`max_health`, `move_speed`, tamaño de sprite → `unit_visual_radius_world`). Sin tipos de armadura ni resistencias — eso pertenece a Combate/Daño. + +| Tipo | `sprite_diameter_px` | `max_health` | `move_speed` (u/s) | `unit_visual_radius_world` | Rol | +|---|---|---|---|---|---| +| Infantería melee | 44 | 40 | 4.0 | 0.4583 | Más resistente, más lenta, visualmente mayor — masa de vanguardia | +| A distancia (ranged) | 32 | 20 | 5.0 | 0.3333 | Más frágil, más rápida, visualmente menor — silueta de hostigamiento | +| Soporte | 40 | 28 | 4.5 | 0.4167 | Punto medio en los tres stats | + +*(Nota de dependencia — ✅ RESUELTO 2026-08-09: los valores de `max_health` (melee = 2× ranged, soporte = 1.4× ranged) fueron confirmados por Combate/Daño: con sus `CombatProfile` los mirror-matchups quedan a ~5s entre sí (melee 5.0s, ranged 4.8s) — los golpes-para-matar se sienten correctos por tipo, no requieren revisión. Ver `combate-dano.md` Formulas.)* + +### Fórmula A — Radio visual desde el tamaño de sprite + +`unit_visual_radius_world = sprite_diameter_px / world_unit_scale / 2` + +| Variable | Símbolo | Tipo | Rango | Descripción | +|---|---|---|---|---| +| Diámetro de sprite | `sprite_diameter_px` | int | 32–48 (art bible) | Diámetro renderizado del sprite de este tipo | +| Escala de mundo | `world_unit_scale` | int | 48 (constante registrada) | px por world unit | +| **Radio visual** | `unit_visual_radius_world` | float | 0.33–0.5 (banda de tropa) | Mitad del diámetro clicable, en world units | + +**Ejemplos**: melee `44/48/2 = 0.4583`; ranged `32/48/2 = 0.3333`; soporte `40/48/2 = 0.4167`. Estos valores se enchufan en la fórmula registrada `select_radius` (owned por Control y Selección) y todos quedan estrictamente por debajo del radio de héroe más pequeño (0.5), manteniendo la consistencia visual de la prioridad héroe>tropa. + +### Fórmula B — Velocidad de movimiento relativa al paneo de cámara + +`move_speed_troop = camera_pan_speed × speed_ratio_troop` + +| Variable | Símbolo | Tipo | Rango | Descripción | +|---|---|---|---|---| +| Velocidad de paneo | `camera_pan_speed` | float | 30 (constante registrada, u/s) | Velocidad de referencia que una tropa nunca debe acercarse/exceder | +| Ratio de velocidad | `speed_ratio_troop` | float | 0.10–0.20 (banda segura) | Fracción de la velocidad de paneo por tipo | +| **Velocidad de tropa** | `move_speed_troop` | float | 3.0–6.0 u/s | Velocidad de movimiento resultante | + +**Ejemplos** (camera_pan_speed=30): melee `30×0.133 = 4.0`; soporte `30×0.150 = 4.5`; ranged `30×0.167 = 5.0`. Incluso la tropa más rápida (ranged, 5.0 u/s) es solo ~17% de la velocidad de paneo — margen de seguridad amplio, la cámara siempre supera a un grupo de tropas. + +### Fórmula C — Tope de refuerzos por tipo (reset por ciclo de Preparación) + +`max_reinforcements_[tipo] = clamp(round(starting_count_[tipo] × reinforcement_ratio), min_reinforcements, max_reinforcements_hard_cap)` + +| Variable | Símbolo | Tipo | Rango | Descripción | +|---|---|---|---|---| +| Conteo inicial del tipo | `starting_count_[tipo]` | int | ≥0 | Cantidad de ese tipo en el `unit_roster` al inicio de la era | +| Ratio de refuerzo | `reinforcement_ratio` | float | 0.15–0.35 (tuning knob) | Fracción de la fuerza inicial reclutable en Preparación | +| Piso de refuerzos | `min_reinforcements` | int | 2 (piso duro) | Evita resultados degenerados de 0-1 en rosters pequeños | +| Techo duro | `max_reinforcements_hard_cap` | int | 8 (techo por tipo) | Evita resultados desbocados en rosters grandes | +| **Tope por tipo** | `max_reinforcements_[tipo]` | int | [2, 8] | Refuerzos de ese tipo reclutables en esta Preparación | + +**Decisiones fijadas**: tope **separado por tipo** (el jugador no puede convertir todos sus refuerzos en el tipo más fuerte) y **reset en cada ciclo de Preparación** (mantiene la tensión de atrición en cada beat). *(El reset por ciclo es provisional — depende del ritmo de Kaiju/Temporizador, aún sin GDD; si una era resulta tener un solo ciclo, el reset es equivalente a un presupuesto único.)* + +**Ejemplo**: era con 10 melee, `reinforcement_ratio=0.25` → `round(10×0.25)=2.5→3` → `clamp(3, 2, 8) = 3` refuerzos melee disponibles esta Preparación. Con 6 ranged → `round(1.5)=2` → `clamp(2,2,8)=2` refuerzos ranged. + +### Fórmula D — Umbral de muerte de tropa + +`is_dead = (current_health <= 0)` + +| Variable | Símbolo | Tipo | Rango | Descripción | +|---|---|---|---|---| +| Vida actual | `current_health` | int | [0, max_health] | Reducida solo por eventos de daño de Combate/Daño | +| ¿Muerta? | `is_dead` | bool | {true, false} | Si la tropa transiciona a `DEAD` este frame | + +Trivial pero formal. **No se captura data de circunstancia de muerte** — sin `killer_id`, ubicación, timestamp ni causa. Esta es la distinción mecánica deliberada respecto a los héroes (Pilar 1) y evita cualquier acoplamiento accidental al sistema de legado/forja. Combate/Daño hace clamp de `current_health` a un mínimo de 0 al aplicar daño, para que `DEAD` dispare exactamente en el cruce por 0. + +### Fórmula E — Tope de banda del jugador y `band_headroom` + +El tope de **tropas** simultáneas vivas del jugador es un valor autorado por era, `max_band_size` (propiedad de Datos de Era/Civilización). Este sistema es el dueño del **enforcement** y expone el margen restante: + +`band_headroom = max(0, max_band_size − alive_troop_count)` + +| Variable | Símbolo | Tipo | Rango | Descripción | +|---|---|---|---|---| +| Tope de banda | `max_band_size` | int | `≥ Σ starting_count_[tipo]` | Máx. tropas simultáneas vivas del jugador esta era. Propiedad de Datos de Era (validado loud-fail al cargar). **NO cuenta héroes** | +| Tropas vivas | `alive_troop_count` | int | `[0, max_band_size]` | Conteo actual de tropas vivas del jugador (todos los tipos), propiedad de este sistema | +| Margen de banda | `band_headroom` | int | `[0, max_band_size]` | Read-only para Reliquias (`rally`) y UI/HUD (RU-3b) | + +**Reconciliación de los dos grifos de refuerzo (cierra F2-B5 / Reliquias OQ-RB7-b):** dos mecánicas distintas alimentan la banda, y ambas respetan `max_band_size` como techo total: +- **Reclutamiento en PREPARACIÓN** (Fórmula C): acotado **por tipo y por ciclo** (`max_reinforcements_[tipo]`) **y además** por el gate global — una recluta se rechaza si `band_headroom == 0`, aunque el tope por tipo aún no se haya alcanzado. +- **`rally` en CLIMAX** (Reliquias): surte `spawned = min(rally_N, band_headroom)` tropas instantáneas; si `band_headroom == 0` es no-op (Reliquias AC-RB40 — la carga se gasta igual). `rally` **no** consume del presupuesto por-tipo/por-ciclo de Preparación; es un grifo independiente, acotado solo por `max_band_size`. + +Los dos son complementarios: el tope por tipo/ciclo throttlea la *recuperación de atrición* en Preparación; `max_band_size` acota el *total en campo* en todo momento (Preparación y CLIMAX). El cap es sobre la masa reemplazable (tropas), no sobre el roster con nombre (héroes). + +**Ejemplo**: `max_band_size=30`, 16 tropas iniciales; tras bajas quedan 22 vivas → `band_headroom=8`. Un `rally` Legendaria (`N=5`) surte 5 (→27 vivas, headroom 3). Un `rally` Sólida (`N=3`) surte `min(3,3)=3` (→30, headroom 0). Un tercer `rally` es no-op, carga gastada. + +## Edge Cases + +- **Si el jugador intenta reclutar más refuerzos de un tipo que su `max_reinforcements_[tipo]`**: la recluta se bloquea al alcanzar el tope; la UI no debe permitir exceder el límite. No es un error, es el comportamiento esperado. +- **Si `starting_count_[tipo]` es 0** (la era no incluye ese tipo): un tipo ausente del `unit_roster` de la era tiene tope de refuerzo **0**, no se ofrece en absoluto — el piso de 2 de la Fórmula C solo aplica a tipos que **sí** existen en la era. +- **Si `band_headroom == 0`** (la banda está en su tope `max_band_size`, Fórmula E): el reclutamiento de Preparación se rechaza globalmente **aunque el tope por tipo aún no se haya alcanzado**, y un `rally` de CLIMAX es no-op (surte 0 tropas). No es un error — es el techo total de banda. En `rally`, la carga se gasta igual (Reliquias AC-RB40). +- **Si una tropa muere mientras estaba seleccionada**: se remueve del conjunto de selección (Control y Selección limpia la referencia el mismo frame, cubierto por su AC-18/19). Si era la última seleccionada, el estado pasa a `NONE_SELECTED`. +- **Si una tropa recibe una orden de movimiento a un destino inalcanzable** (bloqueado por geometría del mapa): `NavigationAgent2D` la lleva al punto navegable más cercano al destino; la tropa entra en `IDLE` ahí, no queda atascada en `MOVING` indefinidamente. +- **Si dos tropas intentan ocupar el mismo tile simultáneamente**: el avoidance del pathfinding de Godot las separa orgánicamente; no hay apilamiento perfecto ni bloqueo mutuo. Sin formación impuesta en el MVP, la resolución exacta la maneja `NavigationAgent2D`. +- **Si la vida de una tropa es reducida por debajo de 0 en un solo golpe** (overkill): Combate/Daño hace clamp a 0; `is_dead` dispara en el cruce por 0. No se preserva vida negativa en el estado. +- **Si el jugador ordena mover una tropa durante `DEATH_HOLD`** (beat de muerte de héroe): el Input ya suprime todo input durante `DEATH_HOLD` (edge case fijado en Input) — la orden nunca llega a este sistema. +- **Si todas las tropas del jugador mueren pero un héroe sigue vivo**: la era continúa; no hay condición de derrota por pérdida total de tropas (los héroes son el eje de la victoria/derrota, no la masa). El jugador simplemente pelea solo con héroes. +- **Si un tipo de tropa tiene `move_speed` mal configurado por encima de `camera_pan_speed`** (error de tuning): se clampa `speed_ratio_troop` a su banda segura [0.10, 0.20] al cargar, garantizando que ninguna tropa pueda superar a la cámara. + +## Dependencies + +**Dependencias hacia arriba (upstream):** + +| Sistema | Tipo | Interfaz | Estado | +|---|---|---|---| +| Datos de Era/Civilización | Dura | Lee `unit_roster` (porción de tropas) de la era ACTIVE para instanciar tipos | ✅ Diseñado | +| Control y Selección de Unidades | Dura (bidireccional) | Provee posición/hitbox y `unit_visual_radius_world`; recibe órdenes de movimiento de él | ✅ Diseñado — contrato confirmado (`select_radius` registrada) | + +**Dependientes hacia abajo (downstream):** + +| Sistema | Tipo | Interfaz | +|---|---|---| +| Combate/Daño | Dura (bidireccional) | Delega cálculo de daño; recibe eventos de daño que reducen vida; notifica `ENGAGED`/`DEAD` | +| Encuentro con Kaiju | Dura | Las tropas son objetivos válidos del kaiju/esbirros; expone posiciones para la IA del kaiju | +| Guardado/Persistencia | Dura | El conteo/estado de tropas vivas es parte del estado de era guardable (las tropas no son entidades de legado) | +| Reliquias/Bendiciones | Dura (efecto `rally`) | El efecto `rally` invoca la API de refuerzo/spawn de tropas de este sistema y surte `min(rally_N, band_headroom)` tropas (**F2-B5 resuelto 2026-08-21**: `max_band_size` autorado por Datos de Era es el techo total; este sistema expone `band_headroom` read-only y hace el enforcement — Fórmula E). `rally` es un grifo independiente del refuerzo por-tipo de Preparación. La firma exacta de la API sigue siendo detalle de implementación (Reliquias OQ-RB7-a). Los modificadores de instancia de bendición se aplican vía la capa `CombatState` de Combate (ADR-0001), no directo a este sistema | + +**Nota bidireccional (actualizada 2026-08-21)**: Combate/Daño, Encuentro con Kaiju y Reliquias/Bendiciones **ya tienen GDD** — sus contratos están confirmados (la fila recíproca de Reliquias se añadió en la reconciliación cross-GDD, cierra parte de Reliquias OQ-RB3). Datos de Era/Civilización, Control y Selección y Guardado/Persistencia también. + +## Tuning Knobs + +| Knob | Símbolo | Tipo | Default | Rango seguro | Qué se rompe en extremos | +|---|---|---|---|---|---| +| Ratio de refuerzo | `reinforcement_ratio` | float | 0.25 | 0.15–0.35 | <0.15: la atrición se siente irrecuperable, frustrante; >0.35: reemplazar bajas es trivial, se pierde la tensión del Pilar 1 | +| Ratio de velocidad por tipo | `speed_ratio_troop` | float | 0.13–0.17 (por tipo) | 0.10–0.20 | <0.10: las tropas se sienten pegajosas/lentas; >0.20: se acercan a superar la cámara, sensación de descontrol | +| Piso de refuerzos | `min_reinforcements` | int | 2 | 1–3 | 0: rosters pequeños quedan sin margen de recuperación; >3: infla refuerzos en rosters diminutos | +| Techo duro de refuerzos por tipo | `max_reinforcements_hard_cap` | int | 8 | 5–12 | <5: limita demasiado en eras grandes; >12: permite masas de refuerzo que diluyen la tensión | + +Los stats por-tipo (`max_health`, `move_speed`, `sprite_diameter_px`) también son tuneables por tipo de tropa (viven en cada `TroopDefinition` Resource), pero son datos de contenido, no knobs globales del sistema. + +**Enforcement**: los valores fuera de rango se clampan al límite más cercano al cargar (consistente con Guardado/Persistencia y Control y Selección). + +**Constantes/valores consumidos** (no se duplican): `camera_pan_speed`=30, `world_unit_scale`=48 y `max_band_size` (autorado por era) → propiedad de Datos de Era/Civilización (registradas). `max_band_size` es el techo total de la Fórmula E; este sistema lo consume y hace el enforcement, no lo posee. + +## Visual/Audio Requirements + +La dirección visual de las tropas ya está autorada a fondo en el **art bible Sección 5.1** (producto del art-director) — se referencia aquí como fuente de verdad en vez de re-especificarla: +- **Silueta**: "astilla vertical" uniforme e indiferenciada — todas las tropas del mismo tipo comparten silueta idéntica (art bible 5.1, 3.1). Sub-diseño deliberado: son la masa gastable. +- **Color**: Hueso (aliado, con vida) por defecto; drena hacia Sangre Vieja al recibir daño (art bible 4.2). Nunca dorado — las tropas no son entidades de legado. +- **Escala**: 32-48px, encajan en tiles de 48px (art bible 8.3). Nivel de LOD 1 únicamente — sin renderizado de detalle Nivel 2 (reservado para permadeath de héroes; las tropas nunca lo activan, art bible 8.4). +- **VFX de este sistema**: feedback de daño (destello breve), muerte de tropa (efecto de caída/disolución acorde al gótico pixelado — pero deliberadamente menos dramático que la muerte de un héroe, sin beat sostenido). Distinción clave: la muerte de una tropa NUNCA debe verse tan significativa como la de un héroe (refuerza el Pilar 1). +- **Audio**: las tropas emiten señales que el sistema de Audio escucha (orden de movimiento, contacto, muerte) — este sistema provee el disparador, no el sonido. La muerte de tropa tiene un sonido más tenue/masivo que la de un héroe. + +📌 **Asset Spec** — Requisitos Visual/Audio definidos. Tras aprobar el art bible, ejecutar `/asset-spec system:sistema-de-tropas` para producir descripciones visuales por-asset, dimensiones y prompts de generación desde esta sección. + +## UI Requirements + +- **Contador de refuerzos por tipo**: durante Preparación, la UI muestra cuántos refuerzos de cada tipo quedan disponibles (`max_reinforcements_[tipo]` menos los ya reclutados). No debe permitir exceder el tope (AC-18). Su diseño visual pertenece a la GDD de UI/HUD (lenguaje diegético del art bible Sección 7). +- **Indicador de vida de tropa**: barra/indicador de vida por tropa (Hueso→Sangre Vieja), renderizado por UI/HUD desde el estado que este sistema expone. +- **Indicador de selección**: cuando una tropa está seleccionada, la UI muestra el contorno de selección (delegado a Control y Selección + UI/HUD). + +📌 **UX Flag — Sistema de Tropas**: Este sistema tiene requisitos de UI (contador de refuerzos, indicador de vida). En Pre-Producción, ejecutar `/ux-design` para especificar el contador de refuerzos de Preparación antes de escribir epics. Las stories que referencien esta UI deben citar `design/ux/[pantalla].md`. + +## Acceptance Criteria + +*(Validado por `qa-lead`. Criterios [BLOQUEADO] no testeables hasta que exista la GDD del sistema mencionado.)* + +### Core Rules + +**AC-01**. Given una era cuyo `unit_roster` lista 3 tipos (melee, ranged, soporte), When la era carga e instancia tropas, Then se instancian exactamente los tipos presentes en `unit_roster` — ni más ni menos — cada uno con su `TroopDefinition` completo, y ningún tipo fuera del roster de la era ACTIVE se instancia. + +**AC-02** [Integration, hueco de fase Preparación]. Given una era en Preparación con `starting_count_melee`=10 y `max_reinforcements_melee`=3, When el jugador recluta, Then se pueden reclutar hasta 3 tropas melee adicionales, nunca se expone producción continua/construcción de base, y los intentos más allá del tope se rechazan. + +**AC-03**. Given dos tropas melee A y B del mismo `TroopDefinition`, When se comparan sus stats, Then todos los campos son idénticos, y ninguna expone nombre individualizado, progresión única, ni campo de historial de muerte. + +**AC-04**. Given una tropa idle seleccionada, When se emite una orden de movimiento a la posición P, Then su `NavigationAgent2D` navega a P independientemente de cualquier otra tropa seleccionada, sin offset de formación. + +**AC-05**. Given un héroe y una tropa ambos dentro del radio de clic del cursor, When el jugador hace clic en esa posición ambigua, Then se selecciona el héroe y no la tropa (cross-ref: prioridad héroe>tropa de Control y Selección) — confirma que `unit_visual_radius_world` nunca anula esa prioridad. + +**AC-06** [BLOQUEANTE, Pilar 1 — test negativo]. Given la vida de una tropa llega a 0, When transiciona a `DEAD`, Then no dispara resolución de permadeath, no se crea entrada de Forja de Legado, y no se registra ningún campo de circunstancia de muerte (`killer_id`, ubicación, timestamp, causa) en ninguna parte; **y como caso de control en la misma corrida de test, un héroe llegando a 0 de vida SÍ dispara el camino de permadeath/legado** — probando que la distinción se hace cumplir, no que está ausente por omisión. *(Test de regresión automatizado. Un refactor futuro que fusione el manejo de muerte de héroe/tropa es la forma más plausible de romper silenciosamente el Pilar 1.)* + +### States and Transitions + +**AC-07**. Given una tropa en `IDLE`, When recibe una orden de movimiento válida, Then transiciona a `MOVING` el mismo frame y `NavigationAgent2D` comienza a navegar. + +**AC-08**. Given una tropa en `MOVING`, When `NavigationAgent2D` reporta llegada, Then transiciona a `IDLE` y deja de emitir input de movimiento. + +**AC-09** [BLOQUEADO — pendiente GDD de Combate/Daño]. Given una tropa en `MOVING` o `IDLE`, When contacta un enemigo o recibe una orden de ataque según el contrato de contacto de Combate/Daño, Then transiciona a `ENGAGED`. *(Interim: mock de señal `contact_detected`, test Logic solo.)* + +**AC-10**. Given una tropa en `IDLE`, `MOVING` o `ENGAGED` con `current_health` en 0, When se procesa la actualización de vida, Then transiciona a `DEAD` incondicionalmente y `DEAD` es terminal. *(Testeable hoy vía mutación directa de vida — no requiere la lógica de aplicación de daño de Combate/Daño.)* + +**AC-11** [BLOQUEADO — pendiente GDD de Combate/Daño]. Given una tropa en `ENGAGED`, When el enemigo muere o se retira según la resolución de Combate/Daño, Then transiciona a `IDLE`. *(Interim mock, test Logic solo.)* + +### Formulas + +**AC-12**. Given `sprite_diameter_px` = 44/32/40 y `world_unit_scale`=48, When se computa `unit_visual_radius_world = sprite_diameter_px/world_unit_scale/2`, Then los resultados son exactamente 0.4583 (melee), 0.3333 (ranged), 0.4167 (soporte), ±0.0001, y los tres estrictamente por debajo del radio de héroe más pequeño (0.5). + +**AC-13**. Given `camera_pan_speed`=30 y `speed_ratio_troop`=0.133 (melee), When se computa `move_speed_troop = camera_pan_speed × speed_ratio_troop`, Then el resultado es 4.0 u/s (±0.01); ratio 0.150 (soporte) → 4.5; ratio 0.167 (ranged) → 5.0. + +**AC-14** [invariante]. Given cada `TroopDefinition` definido y cualquier tipo futuro con `speed_ratio_troop`∈[0.10, 0.20], When se computa `move_speed_troop` para cada uno, Then todo resultado es estrictamente menor a 30 u/s — la tropa más rápida (ranged, 5.0) es ~17% de la velocidad de cámara, y el tope de la banda (0.20×30=6.0) sigue por debajo. *(Test de regresión de roster completo, re-correr al añadir un tipo.)* + +**AC-15**. Given `starting_count_melee`=10, `reinforcement_ratio`=0.25, When se computa `max_reinforcements_melee = clamp(round(10×0.25), 2, 8)`, Then `round(2.5)=3`, `clamp(3,2,8)=3` → tope 3; con `starting_count_ranged`=6 mismo ratio, `round(1.5)=2`, `clamp(2,2,8)=2` → tope 2. + +**AC-16**. Given un `unit_roster` con cero entradas de "soporte" (`starting_count_support`=0), When se computa `max_reinforcements_support`, Then el resultado es 0 — el piso `min_reinforcements` de 2 NO aplica a un tipo ausente de la era, y soporte no se ofrece como opción de refuerzo. + +**AC-17**. Given una tropa con `current_health`=1, When se reduce a 0 por un evento de daño, Then `is_dead` evalúa true y entra a `DEAD` en la misma actualización; con `current_health`=0 exacto (borde), `is_dead` también es true (umbral es `<=`, no `<` estricto). + +### Edge Cases + +**AC-18** (EC-01). Given `max_reinforcements_melee`=3 y 3 ya reclutados este ciclo de Preparación, When se intenta un 4to, Then la acción se rechaza y el conteo queda en 3. *(Lógica de enforcement testeable ahora; el gating de UI "no debe permitir" bloqueado — ver huecos.)* + +**AC-19** (EC-02). Given un tipo con cero entradas en el roster de la era, When se consulta su tope de refuerzo en Preparación, Then el tope es 0 y nunca aparece como opción reclutable. + +**AC-20** (EC-03). Given una tropa en el conjunto de selección, When llega a `DEAD`, Then se remueve del conjunto el mismo frame; si era la única, la selección pasa a `NONE_SELECTED` (cross-ref: Control y Selección AC-18/19). + +**AC-21** (EC-04). Given una orden de movimiento a un destino bloqueado por geometría del mapa, When `NavigationAgent2D` la procesa, Then la tropa navega al punto navegable más cercano y entra a `IDLE` ahí — no queda en `MOVING` indefinidamente. + +**AC-22** (EC-05) [correctitud bloqueante; suavidad visual advisory]. Given dos tropas ordenadas al mismo tile, When ambas llegan en proximidad, Then el avoidance las separa en posiciones no superpuestas sin apilamiento permanente ni deadlock mutuo (ambas eventualmente en `IDLE`, verificable en simulación headless con assertion de timeout). + +**AC-23** (EC-06) [threshold testeable; clamp bloqueado hasta Combate/Daño]. Given `current_health`=5, When se aplica un evento de 999 de daño, Then `current_health` hace clamp a 0 (nunca negativo) e `is_dead` evalúa true esa actualización. + +**AC-24** (EC-07). Given un héroe en `DEATH_HOLD`, When el jugador intenta mover una tropa, Then el Input suprime el input por completo y el Sistema de Tropas nunca recibe un evento `move_commanded`. *(Recomendado: este test vive en la suite de Input, con el handler de movimiento de tropas como receptor espiado.)* + +**AC-25** (EC-08) [BLOQUEADO — nadie posee la evaluación de victoria/derrota de era]. Given todas las tropas en `DEAD` y ≥1 héroe vivo, When se evalúa la condición de victoria/derrota de la era, Then no se dispara derrota por pérdida total de tropas y la era continúa. + +**AC-26** (EC-09). Given un `TroopDefinition` cargado con `speed_ratio_troop`=0.45 (fuera de [0.10, 0.20]), When carga, Then el ratio se clampa a 0.20 y se usa `move_speed_troop` = 30×0.20 = 6.0 u/s. + +### Save/Load (ambos sistemas diseñados — cobertura testeable hoy) + +**AC-31** [Integration con Guardado/Persistencia]. Given N tropas de cada tipo vivas a mitad de era, When el juego guarda y recarga, Then se restauran las mismas N tropas de los mismos tipos/estados (excluyendo data de circunstancia de muerte, que no existe per AC-06). + +### Tuning Knobs (clamping) + +**AC-27**. Given `reinforcement_ratio` configurado en 0.50, When carga, Then se clampa a 0.35 y la Fórmula C usa 0.35. +**AC-28**. Given `speed_ratio_troop` configurado en 0.05, When carga, Then se clampa a 0.10 y `move_speed_troop` = 30×0.10 = 3.0 u/s. +**AC-29**. Given `min_reinforcements` configurado en 0, When carga, Then se clampa a 1. +**AC-30**. Given `max_reinforcements_hard_cap` configurado en 20, When carga, Then se clampa a 12. + +**AC-32** (Fórmula E) [Logic/unit]. Given `max_band_size`=30 y `alive_troop_count`=22, When se consulta `band_headroom`, Then es `8`; y dado un `rally` con `rally_N`=5, When resuelve, Then surte `min(5,8)=5` tropas y `alive_troop_count` pasa a 27. + +**AC-33** (Fórmula E, gate global) [Logic/unit]. Given `band_headroom`=0 (`alive_troop_count == max_band_size`), When el jugador intenta reclutar un refuerzo en Preparación de un tipo cuyo `max_reinforcements_[tipo]` **aún no** está agotado, Then la recluta se rechaza igual (gate global de banda); y When se invoca un `rally` de CLIMAX, Then surte 0 tropas (no-op) — la carga de invocación se gasta igual (cross-ref Reliquias AC-RB40). + +**AC-34** (Fórmula E, validación de config) [Logic/unit]. Given una `EraDefinition` con `max_band_size` menor que `Σ starting_count_[tipo]` del `unit_roster`, When se carga la era, Then falla ruidosamente (loud-fail, el roster inicial no cabe en el tope) — validado en el camino de carga centralizado de era (mismo patrón que los checks cross-resource de Kaiju AC-K74/K85). + +### Clasificación de tipo de story (para el gate de evidencia) + +| AC # | Tipo | Evidencia | Gate | +|---|---|---|---| +| AC-03, AC-06, AC-07, AC-08, AC-10, AC-12 a AC-19, AC-23 (mitad threshold), AC-26 a AC-30 | Logic | Unit test (`tests/unit/troops/`) | Bloqueante | +| AC-01, AC-02, AC-04, AC-05, AC-20, AC-21, AC-22, AC-24, AC-31 | Integration | Integration test / playtest documentado | Bloqueante | +| AC-09, AC-11, AC-25 | Integration | Bloqueado, ver huecos | Bloqueante (diferido) | +| AC-18 (mitad UI), AC-22 (suavidad visual) | UI / Visual | Walkthrough / screenshot + sign-off | Advisory | + +**AC-06 es la excepción a la fuerza normal de gating Logic**: aunque es un test Logic, es **bloqueante y crítico de Pilar 1** — cualquier PR que toque los caminos de muerte de héroe/tropa debe requerir que este test corra, no solo que CI pase incidentalmente. + +### Huecos/no-testeables marcados + +1. **AC-09/AC-11** (transiciones ENGAGED) bloqueados hasta el contrato de contacto/resolución de Combate/Daño. Interim: tests Logic con señal simulada. +2. **AC-23** (clamp de overkill) — el mecanismo de clamp a 0 lo posee Combate/Daño; bloqueado para end-to-end. +3. **AC-25** (pérdida total de tropas ≠ derrota) — bloqueado, y es un hueco de la tabla de dependencias: ningún sistema listado posee la evaluación de victoria/derrota de era (probablemente Transición de Era o Encuentro con Kaiju). + +## Open Questions + +| Pregunta | Owner | Resolución objetivo | +|---|---|---| +| ~~¿Qué sistema posee la evaluación de victoria/derrota de era? (bloquea AC-25 — "pérdida total de tropas ≠ derrota")~~ ✅ **RESUELTO 2026-08-12**: Encuentro con Kaiju (Approved) es el dueño (Regla 9) y confirma directamente que **las tropas NO son condición de derrota** (AC-K53) — la derrota es la muerte del último héroe vivo, aunque queden tropas. Desbloquea AC-25 | game-designer | ✅ Cerrado (Encuentro Regla 9/AC-K53) | +| ¿Qué sistema posee la fase de Preparación y su temporizador? (afecta AC-02 y el reset por-ciclo de refuerzos) — referenciado como "fase de Preparación" pero sin owner en la tabla de dependencias | game-designer | Al diseñar Temporizador de Preparación/Ritual | +| El reset de refuerzos por ciclo de Preparación depende del ritmo de Kaiju/Temporizador (ambos **Approved**). Encuentro con Kaiju usa un reloj de clímax propio (`climax_elapsed_s`) independiente del de Temporizador; el número de ciclos de Preparación por era es un dato de `EraDefinition`. Si una era tiene un solo ciclo, el reset equivale a un presupuesto único | game-designer | Al fijar el ritmo de era (Datos de Era) | +| La interfaz "las tropas exponen posiciones al kaiju" — Encuentro con Kaiju (**Approved**) la concreta: las tropas son candidatos de la selección de objetivo por amenaza del kaiju (Regla 3/F1, con `hero_threat_bonus` favoreciendo héroes sobre tropas). Ya no es una fila de dependencia sin mecánica | game-designer | ✅ Cubierto por Encuentro Regla 3 | +| ~~Los `max_health` por tipo son baselines provisionales — revisar cuando Combate/Daño defina daño-por-golpe~~ ✅ **RESUELTO 2026-08-09**: Combate/Daño confirmó que los baselines se sostienen (mirror-matchups ~5s); sin revisión requerida | systems-designer | ✅ Cerrado | +| El contrato de contacto/aggro (qué dispara `ENGAGED`) lo define Combate/Daño — bloquea AC-09/AC-11 | game-designer | Al diseñar Combate/Daño | diff --git a/design/gdd/systems-index.md b/design/gdd/systems-index.md new file mode 100644 index 0000000000..f331698093 --- /dev/null +++ b/design/gdd/systems-index.md @@ -0,0 +1,183 @@ +# Systems Index: What the Gods Left Behind + +> **Status**: Draft +> **Created**: 2026-07-23 +> **Last Updated**: 2026-08-18 (Reliquias/Bendiciones #12 → **Needs Revision** tras `/review-all-gdds` Fase 2+4: FAIL cross-GDD — centro de gravedad de ~8 clústeres bloqueantes [effective_attack_damage sin dueño, 5 filas de dep faltan, offering_time_scale sin dueño, mecanismo Regla 10, tope de banda rally, persistencia, muerte durante OFFERING]; approved 7→6. Reporte: design/gdd/gdd-cross-review-2026-08-18.md. Prev: #12 → Approved sin re-review; #11 Forja Approved; base_accidental 0.05→0.10) +> **Source Concept**: design/gdd/game-concept.md + +--- + +## Overview + +"What the Gods Left Behind" es un RTS narrativo con elementos roguelite: el jugador comanda una civilización contra un kaiju por era, alternando fases de preparación ritual (posicionamiento, recursos) con clímax de combate explosivo. El núcleo mecánico único del juego es la cadena **Permadeath → Forja de Legado → Reliquias/Bendiciones → Panteón**, que convierte cada muerte permanente de héroe en poder mecánico heredable para eras futuras (Pilares 1 y 2). Los 18 sistemas de este índice cubren desde la infraestructura RTS estándar (control de unidades, combate, datos de era) hasta el motor de legado que le da identidad propia al juego. El MVP se concentra deliberadamente en probar si ese ciclo central — sacrificio con peso, convertido en poder legible — es divertido en una sola civilización antes de expandir a estructura multi-era. + +--- + +## Systems Enumeration + +| # | System Name | Category | Priority | Status | Design Doc | Depends On | +|---|-------------|----------|----------|--------|------------|------------| +| 1 | Input | Core | MVP | In Review — design-review 2026-08-19: NEEDS REVISION → 7 bloqueantes aplicados misma sesión; pendiente re-review en sesión limpia (log: design/gdd/reviews/input-review-log.md) | design/gdd/input.md | — | +| 2 | Datos de Era/Civilización | Core | MVP | Designed | design/gdd/datos-de-era-civilizacion.md | — | +| 3 | Guardado/Persistencia | Persistence | MVP | Designed | design/gdd/guardado-persistencia.md | — | +| 4 | Control y Selección de Unidades | Core | MVP | Designed | design/gdd/control-y-seleccion-de-unidades.md | Input | +| 5 | Sistema de Tropas | Gameplay | MVP | Designed | design/gdd/sistema-de-tropas.md | Datos de Era/Civilización | +| 6 | Sistema de Héroes | Gameplay | MVP | Approved | design/gdd/sistema-de-heroes.md | Datos de Era/Civilización | +| 7 | Temporizador de Preparación/Ritual | Gameplay | MVP | Approved | design/gdd/temporizador-de-preparacion-ritual.md | Datos de Era/Civilización | +| 8 | Combate/Daño | Gameplay | MVP | Approved | design/gdd/combate-dano.md | Sistema de Tropas, Sistema de Héroes, Control y Selección de Unidades | +| 9 | Encuentro con Kaiju (incl. esbirros) | Gameplay | MVP | Approved | design/gdd/encuentro-con-kaiju.md | Combate/Daño, Datos de Era/Civilización, Temporizador de Preparación/Ritual | +| 10 | Permadeath | Legado | MVP | In Review — pasada de reconciliación cross-GDD (OQ-P6) completa 2026-08-16 (los 6 ítems aplicados a Héroes/Guardado/Kaiju/Datos de Era); pendiente re-review formal en sesión limpia antes de Approved (además: spike de motor 4.6 OQ-P5, OQ-P2, Forja de Legado sin GDD) | design/gdd/permadeath.md | Sistema de Héroes, Combate/Daño, Datos de Era/Civilización | +| 11 | Forja de Legado | Legado | MVP | Approved (design-review 2026-08-17: NEEDS REVISION → 2 bloqueantes + 8 recomendados aplicados y aceptados) | design/gdd/forja-de-legado.md | Permadeath, Guardado/Persistencia, Datos de Era/Civilización | +| 12 | Reliquias/Bendiciones (incl. balance) | Legado | MVP | Needs Revision | design/gdd/reliquias-bendiciones.md | Forja de Legado, Combate/Daño, Temporizador de Preparación/Ritual, Sistema de Tropas, Sistema de Héroes, Guardado/Persistencia, Datos de Era/Civilización, Input | +| 13 | UI/HUD | UI | MVP | Approved (design-review 2026-08-14: NEEDS REVISION → 3 bloqueantes aplicados y aceptados) | design/gdd/ui-hud.md | Combate/Daño, Reliquias/Bendiciones, Sistema de Héroes, Control y Selección de Unidades, Input | +| 14 | Salón Conmemorativo/Panteón | Legado | Vertical Slice | Not Started | — | Forja de Legado, Reliquias/Bendiciones, Guardado/Persistencia | +| 15 | Transición de Era | Legado | Vertical Slice | Not Started | — | Datos de Era/Civilización, Guardado/Persistencia, Forja de Legado | +| 16 | Audio | Audio | Vertical Slice | Not Started | — | Temporizador de Preparación/Ritual, Combate/Daño, Permadeath | +| 17 | Narrativa Ambiental (inferred) | Narrative | Alpha | Not Started | — | Datos de Era/Civilización, Forja de Legado | +| 18 | Accesibilidad (inferred) | Meta | Alpha | Not Started | — | UI/HUD | + +--- + +## Servicios de Soporte de Infraestructura (Core, no numerados) + +Introducidos por ADRs de arquitectura, no por `/map-systems`. No son sistemas de +gameplay; son servicios transversales que muchos sistemas consumen. No se numeran +en la enumeración de 18 para no romper referencias existentes. + +| Servicio | Categoría | ADR | Provee | Consumido por | +|----------|-----------|-----|--------|---------------| +| TimeControl | Core (soporte) | ADR-0002 (Accepted 2026-08-21) | Régimen de tiempo de juego: `time_scale`, `game_time_paused`, `game_tick`; pila de precedencia `PAUSED>SLOWED>NORMAL` vía `request_regime`/`release_regime` | Permadeath (solicita PAUSED), Reliquias/Bendiciones (solicita SLOWED), Combate/Daño, Encuentro con Kaiju, Input, Temporizador (lectores read-only) | + +--- + +## Categories + +| Category | Description | Systems in This Project | +|----------|-------------|--------------------------| +| **Core** | Infraestructura base de la que depende todo | Input, Datos de Era/Civilización, Control y Selección de Unidades; **TimeControl** (servicio de soporte, ADR-0002) | +| **Gameplay** | Los sistemas que hacen divertido el loop RTS | Sistema de Tropas, Sistema de Héroes, Temporizador de Preparación/Ritual, Combate/Daño, Encuentro con Kaiju | +| **Legado** | El motor narrativo-mecánico único del juego (Pilares 1 y 2) | Permadeath, Forja de Legado, Reliquias/Bendiciones, Salón Conmemorativo/Panteón, Transición de Era | +| **Persistence** | Guardado y continuidad de estado | Guardado/Persistencia | +| **UI** | Pantallas de información al jugador | UI/HUD | +| **Audio** | Sonido y música | Audio | +| **Narrative** | Entrega de historia sin texto/cutscenes (Pilar 4) | Narrativa Ambiental | +| **Meta** | Sistemas fuera del loop central | Accesibilidad | + +--- + +## Priority Tiers + +| Tier | Definition | Target Milestone | +|------|------------|-------------------| +| **MVP** | Requerido para probar "¿es divertido el ciclo preparación→clímax→legado?" en una sola civilización | Primer prototipo jugable (3-4 meses) | +| **Vertical Slice** | Requerido para conectar 2+ eras y validar la transición de legado | Vertical slice (2-3 meses adicionales) | +| **Alpha** | Todas las civilizaciones/kaijus, sistemas completos en forma rugosa | Alpha (6-8 meses adicionales) | +| **Full Vision** | Pulido, casos borde, contenido completo | Beta/Release (4-6 meses adicionales) | + +--- + +## Dependency Map + +### Foundation Layer (sin dependencias) + +1. **Input** — recibe la entrada cruda del jugador, nada depende de él para existir +2. **Datos de Era/Civilización** — define datos estáticos (rosters, kaiju, tileset) que todo lo demás consume; cuello de botella +3. **Guardado/Persistencia** — infraestructura de bajo nivel que otros sistemas usan para escribir/leer estado + +### Core Layer (depende de Foundation) + +1. **Control y Selección de Unidades** — depende de: Input +2. **Sistema de Tropas** — depende de: Datos de Era/Civilización +3. **Sistema de Héroes** — depende de: Datos de Era/Civilización +4. **Temporizador de Preparación/Ritual** — depende de: Datos de Era/Civilización + +### Feature Layer (depende de Core) + +1. **Combate/Daño** — depende de: Sistema de Tropas, Sistema de Héroes, Control y Selección de Unidades +2. **Encuentro con Kaiju** — depende de: Combate/Daño, Datos de Era/Civilización, Temporizador de Preparación/Ritual +3. **Permadeath** — depende de: Sistema de Héroes, Combate/Daño, **Datos de Era/Civilización** (lee `get_hero_roster_count()` sobre era-`LOADED`); solicita el régimen `PAUSED` a **TimeControl** (servicio de soporte) +4. **Forja de Legado** — depende de: Permadeath +5. **Reliquias/Bendiciones** — depende de: Forja de Legado, Combate/Daño, Temporizador de Preparación/Ritual, Sistema de Tropas, Sistema de Héroes, Guardado/Persistencia, Datos de Era/Civilización, Input (contexto `BLESSING_SELECT`); solicita el régimen `SLOWED` a **TimeControl** *(deps recíprocas reconciliadas 2026-08-21, cierra OQ-RB3 salvo la fila de Guardado que lleva ADR-0003)* +6. **Transición de Era** — depende de: Datos de Era/Civilización, Guardado/Persistencia, Forja de Legado +7. **Salón Conmemorativo/Panteón** — depende de: Forja de Legado, Reliquias/Bendiciones, Guardado/Persistencia +8. **Narrativa Ambiental** — depende de: Datos de Era/Civilización, Forja de Legado + +### Presentation Layer (depende de Feature) + +1. **UI/HUD** — depende de: Combate/Daño, Reliquias/Bendiciones, Sistema de Héroes, Control y Selección de Unidades, Input (acciones `ui_*`, `active_input_method`, `input_lockout_active`) +2. **Audio** — depende de: Temporizador de Preparación/Ritual, Combate/Daño, Permadeath + +### Polish Layer (depende de todo) + +1. **Accesibilidad** — depende de: UI/HUD + +--- + +## Recommended Design Order + +| Order | System | Priority | Layer | Agent(s) | Est. Effort | +|-------|--------|----------|-------|----------|-------------| +| 1 | Input | MVP | Foundation | game-designer | S | +| 2 | Datos de Era/Civilización | MVP | Foundation | game-designer, world-builder | M | +| 3 | Guardado/Persistencia | MVP | Foundation | game-designer, engine-programmer | S | +| 4 | Control y Selección de Unidades | MVP | Core | game-designer | S | +| 5 | Sistema de Tropas | MVP | Core | game-designer, systems-designer | M | +| 6 | Sistema de Héroes | MVP | Core | game-designer, systems-designer | M | +| 7 | Temporizador de Preparación/Ritual | MVP | Core | game-designer | S | +| 8 | Combate/Daño | MVP | Feature | systems-designer | M | +| 9 | Encuentro con Kaiju | MVP | Feature | systems-designer, game-designer | L | +| 10 | Permadeath | MVP | Feature | game-designer | M | +| 11 | Forja de Legado | MVP | Feature | game-designer, systems-designer | L | +| 12 | Reliquias/Bendiciones | MVP | Feature | systems-designer, economy-designer | L | +| 13 | UI/HUD | MVP | Presentation | ux-designer, game-designer | M | +| 14 | Transición de Era | Vertical Slice | Feature | game-designer, narrative-director | M | +| 15 | Salón Conmemorativo/Panteón | Vertical Slice | Feature | game-designer, ux-designer | M | +| 16 | Audio | Vertical Slice | Presentation | audio-director, sound-designer | M | +| 17 | Narrativa Ambiental | Alpha | Feature | world-builder, level-designer | S | +| 18 | Accesibilidad | Alpha | Polish | accessibility-specialist, ux-designer | S | + +*(Effort: S = 1 sesión, M = 2-3 sesiones, L = 4+ sesiones)* + +--- + +## Circular Dependencies + +- **Ninguna a nivel de runtime/inicialización.** Aclaración (reconciliación 2026-08-21): varias aristas se etiquetan "Dura (bidireccional)" en los GDDs, pero eso denota **reciprocidad de documentación** (la regla "si A depende de B, la GDD de B debe mencionar a A"), **no** un ciclo de arranque. Los pares que parecen mutuos se resuelven por ownership claro: + - **Combate ↔ Tropas/Héroes**: Tropas/Héroes poseen `current_health` y el estado de vida; Combate posee el cálculo de daño y solo *notifica* transiciones vía método del dueño. No hay ciclo de init (los datos de unidad existen antes que Combate resuelva un golpe). + - **Reliquias → Combate**: Reliquias deposita modificadores vía la API de `CombatState` (ADR-0001); Combate no depende de Reliquias para su cálculo base. Unidireccional. + - **TimeControl** (servicio de soporte) lo *solicitan* Permadeath/Reliquias y lo *leen* Combate/Kaiju/Input/Temporizador; TimeControl no depende de ninguno de ellos — es una hoja del grafo, sin ciclo. +- Ningún par se describe como dependencia dura de arranque en ambos sentidos. + +--- + +## High-Risk Systems + +| System | Risk Type | Risk Description | Mitigation | +|--------|-----------|-------------------|------------| +| Forja de Legado | Design | Mapeo muerte→reliquia sin precedente claro en RTS; podría no sentirse balanceado o significativo | Prototipar temprano (`/prototype`) antes de comprometerse a la GDD completa | +| Reliquias/Bendiciones | Design/Scope | El pool de reliquias podría volverse difícil de balancear a medida que crece entre eras | Definir tope de pool y reglas de escalado explícitas en la GDD; revisar en cada era añadida | +| Encuentro con Kaiju | Technical | IA de kaiju + esbirros + contraste de escala brutal (art bible Principio B) en Godot 4.6, versión post-cutoff del modelo | Cruzar referencia con `docs/engine-reference/godot/VERSION.md`; prototipar el encuentro base temprano | +| Guardado/Persistencia | Technical | Persistencia de legado entre eras es una arquitectura de datos nueva sin muchos referentes RTS directos (ya flageado en el concepto) | Diseñar el esquema de datos temprano (orden #3), validar con un ADR antes de construir sobre él | + +--- + +## Progress Tracker + +| Metric | Count | +|--------|-------| +| Total systems identified | 18 | +| Design docs started | 13 | +| Design docs reviewed | 9 | +| Design docs approved | 6 | +| MVP systems designed | 13/13 | +| Vertical Slice systems designed | 0/3 | + +--- + +## Next Steps + +- [ ] Review and approve this systems enumeration +- [ ] Design MVP-tier systems first (use `/design-system [system-name]`) +- [ ] Run `/design-review` on each completed GDD +- [ ] Run `/gate-check pre-production` when MVP systems are designed +- [ ] Validate the highest-risk systems with `/vertical-slice` before committing to Production diff --git a/design/gdd/temporizador-de-preparacion-ritual.md b/design/gdd/temporizador-de-preparacion-ritual.md new file mode 100644 index 0000000000..dc5de22735 --- /dev/null +++ b/design/gdd/temporizador-de-preparacion-ritual.md @@ -0,0 +1,352 @@ +# Temporizador de Preparación/Ritual + +> **Status**: Approved (revisado post design-review, 5 bloqueantes aplicados, aceptado sin re-review — 2026-08-09) +> **Author**: usuario + agentes +> **Last Updated**: 2026-08-09 +> **Implements Pillar**: Preparación Ritual, Clímax Explosivo (Pilar 3 — sistema eje) + +## Overview + +El Temporizador de Preparación/Ritual es el reloj maestro que gobierna el ritmo central del juego (Pilar 3): la alternancia entre una **fase de Preparación** —pausada, de posicionamiento de tropas y decisiones rituales— y un **Clímax** explosivo cuando el kaiju de la era llega. A nivel de datos, es una máquina de estados de fase que lee su configuración de timing por era desde `EraDefinition` (Datos de Era/Civilización) y expone, en todo momento, en qué fase está el encuentro y cuánto falta para el clímax. A nivel de jugador, es un **temporizador visible** cuya cuenta atrás convierte la calma en tensión: el jugador ve al kaiju acercarse y decide, contra ese reloj, cuándo mover fuerzas, cuándo invocar un recurso limitado y —para los héroes— cuándo una muerte vale más. Sin este sistema no existiría el Pilar 3: no habría una estructura que separe la construcción pausada del enfrentamiento intenso, ni la fuente de tiempo compartida de la que otros sistemas derivan sus propias apuestas (notablemente la "estaca del kaiju" que pondera la calidad de reliquia en el Sistema de Héroes). Es puro pacing hecho sistema: no calcula combate ni daño, solo *cuándo* las cosas pasan y con cuánta presión. + +## Player Fantasy + +**El jugador vive bajo un reloj que no puede detener.** El kaiju siempre llega — eso es fijo, como es fija la muerte de un héroe. Lo que el temporizador le da al jugador no es la esperanza de evitar el clímax, sino la pregunta de **cómo gasta el tiempo que le queda antes de que llegue**. Cada segundo de la fase de Preparación es una moneda: ¿coloco una tropa más, invoco una reliquia ahora o la guardo, comprometo a este héroe mientras la estaca aún es baja o espero a que el reloj toque fondo? La calma nunca es ociosa — el reloj visible la carga de decisión. + +Es la fantasía del **general en la víspera de la batalla**: la tensión no viene de la sorpresa (sabes que la bestia viene, y aproximadamente cuándo) sino de la administración de una cuenta atrás inexorable. El pico de dread es máximo justo antes del choque, cuando el margen para reposicionar se agota; el Clímax es la liberación de toda esa presión acumulada. Este ritmo —calma preñada de decisión → estallido breve e intenso— es el latido que el Pilar 3 promete ("El ritmo alterna entre construcción/posicionamiento pausado y enfrentamientos breves e intensos contra el kaiju"), y el temporizador es su corazón: sin él, la Preparación sería un sandbox sin urgencia y el combate un evento sin crescendo. + +La honestidad de la fantasía descansa en una regla: **el reloj informa, nunca miente ni sorprende injustamente.** El jugador siempre puede leer cuánto falta y en qué fase está, porque toda la tensión de diseño (Pilar 3) es la de *decidir bajo un límite conocido*, no la de ser emboscado por un límite oculto. Un clímax que llega sin aviso legible no es tensión ritual — es un gotcha, y se rediseña. (Esta legibilidad rima con la ventana de decisión de ~30s que el Sistema de Héroes exige para que una muerte sea "bien gastada".) + +## Detailed Design + +### Core Rules + +1. **Reloj por era, data-driven**: el temporizador lee su configuración de timing desde el `EraDefinition` de la era `ACTIVE` (Datos de Era/Civilización) — al menos `approach_duration_s` (tiempo total del acercamiento del kaiju) y `imminent_window_s` (duración de la fase final de aviso). Ningún valor de timing está hardcodeado; cada era define su propio ritmo. + +2. **Tres fases, una sola cuenta atrás monótona**: `PREPARATION → IMMINENT → CLIMAX`. Un único reloj cuenta desde `approach_duration_s` hasta 0; las fases son umbrales sobre ese reloj, no relojes separados. `IMMINENT` es la ventana final (`imminent_window_s`) antes del clímax. + +3. **Dueño único de `kaiju_timer_remaining_ratio`** (resuelve Héroes OQ-3): este sistema es la **fuente de verdad** del "cuán cerca del clímax estamos". Expone `kaiju_timer_remaining_ratio ∈ [0,1]` = `tiempo_restante_al_clímax / approach_duration_s`, que va de **1.0** al inicio de `PREPARATION` a **0.0** al inicio de `CLIMAX`. El Sistema de Héroes deriva su input `T = 1 − kaiju_timer_remaining_ratio` leyendo este valor en el frame de muerte; Encuentro con Kaiju lo **consume** (no lo posee). + +4. **Puramente temporizado, monótono**: el clímax se dispara solo cuando el reloj llega a 0. El jugador **no** puede acelerar, pausar (fuera del menú de pausa global) ni revertir la cuenta atrás desde el gameplay — es "el reloj que no puedes detener" (Player Fantasy). No hay trigger anticipado en el MVP (ver Open Questions). + +5. **Legibilidad garantizada**: la fase actual y el tiempo restante son siempre consultables y se exponen a UI/HUD. El clímax nunca llega sin que la fase `IMMINENT` lo haya anunciado de forma legible (regla de la fantasía: informar, nunca emboscar). + +6. **`CLIMAX` fija el ratio en 0**: al entrar a `CLIMAX`, `kaiju_timer_remaining_ratio` se fija en 0.0 y **permanece** en 0 durante todo el combate contra el kaiju — toda muerte durante el clímax tiene estaca máxima (`T=1`). Este sistema no cronometra el interior del combate del clímax (duración de la pelea, enrage, etc.) — eso pertenece a Encuentro con Kaiju. + +7. **Solo pacing, no combate**: el temporizador emite **señales de cambio de fase** (`entered_imminent`, `entered_climax`) y expone el ratio; **no** instancia al kaiju, no corre IA ni calcula daño. Los consumidores (Encuentro con Kaiju, UI, Audio) reaccionan a sus señales. Separación de responsabilidades: este sistema decide *cuándo*, no *qué pasa*. + +### States and Transitions + +| Estado | Descripción | Transición desde | Transición hacia | +|---|---|---|---| +| `PREPARATION` | Fase calma; reloj corriendo; ratio de 1.0 hasta `(1 − imminent_window_s/approach_duration_s)` | Era pasa a `ACTIVE` (estado inicial) | `IMMINENT` | +| `IMMINENT` | Ventana final de aviso (`imminent_window_s`); reloj corriendo hacia 0; ratio bajando a 0 | `PREPARATION` (al cruzar el umbral de tiempo) | `CLIMAX` | +| `CLIMAX` | Kaiju llegó; ratio fijado en 0; el reloj de acercamiento deja de contar | `IMMINENT` (reloj llega a 0) | — (terminal para este sistema; el fin de la era lo evalúa otro sistema) | + +- Las transiciones `PREPARATION→IMMINENT→CLIMAX` son unidireccionales y disparadas solo por el paso del tiempo. No hay retroceso de fase. +- El fin del `CLIMAX` (victoria/derrota de la era) **no lo posee este sistema** — mismo hueco abierto que en Tropas y Héroes; lo evalúa Encuentro con Kaiju o Transición de Era. Este sistema solo lleva el encuentro *hasta* el clímax. + +### Interactions with Other Systems + +| Sistema | Dirección | Interfaz | +|---|---|---| +| Datos de Era/Civilización | Datos → este sistema | Lee `approach_duration_s` e `imminent_window_s` de la era `ACTIVE` | +| Encuentro con Kaiju | este sistema → Kaiju | Consume las señales `entered_imminent`/`entered_climax` y `kaiju_timer_remaining_ratio` para cronometrar aparición/comportamiento del kaiju. **No posee el ratio.** ✅ **Approved** — contrato confirmado (Encuentro AC-K04, consume read-only) | +| Sistema de Héroes | este sistema → Héroes | Fuente de verdad de `kaiju_timer_remaining_ratio`; el snapshot de muerte de héroe (D/T/P) lee este valor para el input `T`. **Resuelve Héroes OQ-3** | +| UI/HUD | este sistema → UI | Provee `current_phase` y `time_remaining_to_climax_s` para el temporizador visible y la escalada de aviso de `IMMINENT` *(Provisional — sin GDD)* | +| Audio | este sistema → Audio | Las señales de cambio de fase y el ratio dirigen el crescendo musical/ambiental prep→clímax *(Provisional — sin GDD)* | +| Guardado/Persistencia | bidireccional | `elapsed_s` y la fase actual son parte del estado guardable (la fase se recomputa vía F3 al cargar, nunca se confía en el valor de fase persistido — ver Edge Cases y AC-T23); en autosave (p. ej. muerte de héroe) o guardado manual durante `PREPARATION`/`IMMINENT` se capturan en ese frame y al recargar el reloj reanuda desde `elapsed_s`. Contrato confirmado en `design/gdd/guardado-persistencia.md` (Rule 2/4, AC-05/AC-10) | + +> **Nota de distinción (evita conflación cross-system)**: la ventana `IMMINENT` es una **escalada global del encuentro** ("el kaiju está por golpear"), distinta de la ventana de decisión de ~30s *por héroe* del Sistema de Héroes (AC-H29), que se basa en el `time_to_death` de un héroe concreto provisto por Encuentro con Kaiju, no en este reloj global. Son señales relacionadas pero separadas. + +## Formulas + +Este sistema no calcula combate ni daño — sus fórmulas son puramente de pacing: convierten el tiempo transcurrido en una fase, un ratio de estaca y valores de escalada para UI/Audio. + +**Convención de comparación de punto flotante (aplica a toda AC de este documento)**: salvo que se indique lo contrario, las comparaciones de valores float no enteros usan una tolerancia de ±0.0001 (p. ej. `assert_almost_eq` en GUT), nunca igualdad estricta. Varios de los ratios resultantes de F4 son fracciones periódicas no representables exactamente en punto flotante binario (p. ej. `15/45 = 0.3̄3`, `35/45 = 0.7̄7`); comparaciones contra un literal decimal truncado (`0.3333`) fallarían con una implementación correcta si se exigiera igualdad estricta. + +### F1 — `time_remaining_to_climax_s` + +`time_remaining_to_climax_s = max(approach_duration_s − elapsed_s, 0)` + +| Variable | Símbolo | Tipo | Rango | Descripción | +|---|---|---|---|---| +| Tiempo transcurrido | `elapsed_s` | float | [0, ∞) | Segundos desde que inició `PREPARATION` | +| Duración del acercamiento | `approach_duration_s` | float (config era) | >0 | Tiempo total del acercamiento (validado al cargar, loud-fail si ≤0) | +| **Tiempo restante** | `time_remaining_to_climax_s` | float | [0, approach_duration_s] | Valor de la cuenta atrás visible (UI) | + +**Ejemplo**: `approach_duration_s=600`, `elapsed_s=30` → `time_remaining_to_climax_s = 570`. + +### F2 — `kaiju_timer_remaining_ratio` (salida clave — consumida por Héroes) + +`kaiju_timer_remaining_ratio = clamp(time_remaining_to_climax_s / approach_duration_s, 0.0, 1.0)` + +| Variable | Símbolo | Tipo | Rango | Descripción | +|---|---|---|---|---| +| **Ratio de tiempo restante** | `kaiju_timer_remaining_ratio` | float | [0.0, 1.0] | 1.0 al inicio de `PREPARATION` → 0.0 al inicio de `CLIMAX`; fijado en 0 durante `CLIMAX` | + +Es la **fuente de verdad** del "cuán cerca del clímax". El Sistema de Héroes deriva `T = 1 − kaiju_timer_remaining_ratio` (Héroes Fórmula H4). **Output range**: [0.0, 1.0] siempre; el `clamp` y el `max` de F1 garantizan que nunca sea negativo ni >1. + +**Ejemplo**: con `time_remaining_to_climax_s=570`, `approach_duration_s=600` → `ratio = 570/600 = 0.95` → Héroes `T = 0.05`. + +### F3 — Predicado de fase + +``` +imminent_threshold_s = approach_duration_s − imminent_window_s + +phase = PREPARATION si elapsed_s < imminent_threshold_s + IMMINENT si imminent_threshold_s ≤ elapsed_s < approach_duration_s + CLIMAX si elapsed_s ≥ approach_duration_s +``` + +| Variable | Símbolo | Tipo | Rango | Descripción | +|---|---|---|---|---| +| Ventana de aviso | `imminent_window_s` | float (config era) | 0 < x < approach_duration_s | Duración de la fase final `IMMINENT` (validado al cargar) | +| Umbral de aviso | `imminent_threshold_s` | float (derivado) | (0, approach_duration_s) | Frontera de `elapsed_s` para PREPARATION→IMMINENT | + +**Convención de boundary**: inclusive-lower. En `elapsed_s == imminent_threshold_s` exacto la fase ya es `IMMINENT`; en `elapsed_s == approach_duration_s` exacto ya es `CLIMAX`. No hay frame ambiguo entre fases. + +### F4 — `imminent_local_progress` (derivado — para escalada de UI/Audio) + +`imminent_local_progress = clamp((elapsed_s − imminent_threshold_s) / imminent_window_s, 0.0, 1.0)` + +| Variable | Símbolo | Tipo | Rango | Descripción | +|---|---|---|---|---| +| **Progreso local de IMMINENT** | `imminent_local_progress` | float | [0.0, 1.0] | 0.0 al entrar a `IMMINENT`, 1.0 al inicio de `CLIMAX`; monótono. Solo significativo mientras `phase == IMMINENT` | + +Alimenta rampas de crescendo (audio) y escalada visual del aviso (UI) sin exponer segundos crudos. + +### Valores por defecto (MVP) + +Ambos son tuning knobs por era (ver Tuning Knobs), no constantes fijas: + +| Knob | Default MVP | Rango seguro | Justificación | +|---|---|---|---| +| `approach_duration_s` | **600 (10 min)** | 300–1800 (5–30 min) | El loop corto del concepto es 5-15 min; 10 min permite 1-2 ciclos de recursos/posicionamiento antes del clímax, dentro de una era de 30-120 min | +| `imminent_window_s` | **45** | 30–90 (siempre < 20% de `approach_duration_s`) | Contiene con margen la ventana de decisión de ~30s por héroe (Héroes AC-H29); corto para leerse como "recta final", no una segunda calma | + +**Cross-check con Héroes** (Fórmula H4, ejemplos canónicos): muerte temprana `elapsed_s=30` → `ratio=0.95` → `T=0.05` ✓ (Ejemplo 2 de Héroes); muerte climática `elapsed_s=570` (dentro de `IMMINENT`, pues `555 ≤ 570 < 600`) → `ratio=0.05` → `T=0.95` ✓ (Ejemplo 3); muerte en `CLIMAX` → `ratio=0` → `T=1` ✓ (Regla 6). + +## Edge Cases + +- **Si `approach_duration_s ≤ 0`** (error de datos de era): falla ruidosamente al cargar el `EraDefinition` (F1/F2 dividirían por cero) — nunca se corre el reloj con una duración inválida. Mismo patrón loud-fail que Datos de Era con `kaiju_definition` rota. +- **Si `imminent_window_s ≥ approach_duration_s` o `imminent_window_s ≤ 0`** (error de datos): falla ruidosamente al cargar — dejaría a `PREPARATION` sin duración o produciría un `imminent_threshold_s` fuera de rango. Se exige `0 < imminent_window_s < approach_duration_s`. +- **Si `elapsed_s` supera `approach_duration_s`** (ya en `CLIMAX`): `time_remaining_to_climax_s` queda clampeado a 0 por el `max(...,0)` de F1 y `kaiju_timer_remaining_ratio` en 0 por el `clamp` de F2. El reloj deja de avanzar `elapsed_s` internamente al entrar a `CLIMAX` (evita crecimiento no acotado), pero aunque avanzara, los guards ya garantizan valores válidos. +- **Si un solo paso de tiempo (delta grande, p. ej. tras un stall) cruzaría de `PREPARATION` directo a `CLIMAX` saltándose la ventana `IMMINENT`**: el sistema **igual emite `entered_imminent` y luego `entered_climax` en orden** dentro de ese paso — ningún consumidor (Audio, UI, Kaiju) puede perderse la señal de `IMMINENT` por un hipo de framerate. Las señales de fase se emiten en secuencia, nunca se omiten. +- **El reloj usa tiempo de juego, no tiempo real**: `elapsed_s` acumula el delta de **tiempo de juego** derivado del servicio Core **TimeControl** (ADR-0002, Accepted 2026-08-21) — el Temporizador consume `TimeControl.game_tick`/`time_scale`, no conteo de frames ni reloj de pared. Como `game_tick` se **congela** bajo `PAUSED` (beat `DEATH_HOLD` de Permadeath) y avanza **escalado** bajo `SLOWED` (oferta de Reliquias), el reloj del kaiju se detiene/ralentiza con esos regímenes "gratis", sin cableado directo a Permadeath/Reliquias. Esto **cierra OQ-P4/F2-W4** (antes el `game_time_paused` de Permadeath no llegaba a este sistema). El pacing sigue siendo independiente del framerate. +- **Menú de pausa global (régimen ORTOGONAL a TimeControl)**: si el jugador abre el menú de pausa global (`SceneTree.paused=true`, AC-T22), `elapsed_s` no avanza y reanuda al despausar. Ese es un régimen de motor distinto del régimen de tiempo de juego de TimeControl (que NO usa `SceneTree.paused`): la pausa de menú detiene todo el árbol; el régimen `PAUSED` de TimeControl es cooperativo y selectivo. La pausa de menú no es un "trigger anticipado" ni una vía de gameplay para detener el reloj (Regla 4). +- **Si ocurre un autosave (p. ej. muerte de héroe) durante `IMMINENT` o `CLIMAX`**: el `elapsed_s` y la fase se capturan en ese frame como parte del estado guardado; esto es coherente con el snapshot de Héroes, que lee `kaiju_timer_remaining_ratio` en el mismo frame de muerte. +- **Si se recarga una partida guardada a mitad del acercamiento**: el reloj reanuda desde el `elapsed_s` persistido, y la **fase se recomputa determinísticamente** desde `elapsed_s` vía F3 (no se confía en una fase almacenada que pudiera haber derivado) — garantiza consistencia fase↔tiempo tras la carga. +- **Si se consulta `kaiju_timer_remaining_ratio` antes de que la era esté `ACTIVE`** (reloj no iniciado): el temporizador solo corre mientras su era está `ACTIVE`; fuera de eso no expone un ratio de encuentro en curso (los consumidores no deben leerlo sin un encuentro activo). +- **Si `CLIMAX` se alcanza pero nadie evalúa el fin de la era** (victoria/derrota es de otro sistema): el reloj permanece en `CLIMAX` con ratio=0 indefinidamente — no es función de este sistema terminar la era, solo llevarla hasta el clímax (ver States and Transitions). + +## Dependencies + +**Dependencias hacia arriba (upstream) — lo único que este sistema necesita para funcionar:** + +| Sistema | Tipo | Interfaz | Estado | +|---|---|---|---| +| Datos de Era/Civilización | Dura | Lee `approach_duration_s` e `imminent_window_s` de la era `ACTIVE` | ✅ Diseñado — Datos de Era ya lista a este sistema como consumidor de "configuración de timing por era"; contrato bidireccional confirmado | + +**Dependientes hacia abajo (downstream — sistemas que dependen de este):** + +| Sistema | Tipo | Interfaz | Estado | +|---|---|---|---| +| Sistema de Héroes | Dura | Lee `kaiju_timer_remaining_ratio` para su input `T` (snapshot en el frame de muerte). **Este sistema es el dueño del ratio** | ✅ Approved — Héroes OQ-3 ya resuelto (2026-08-07), atribuye correctamente el ratio a este sistema | +| Encuentro con Kaiju | Dura | Consume `kaiju_timer_remaining_ratio` y las señales `entered_imminent`/`entered_climax` para cronometrar aparición/comportamiento del kaiju | ✅ Approved — `encuentro-con-kaiju.md` (AC-K04, consume read-only) | +| Reliquias/Bendiciones | Dura (solo lectura) | Lee `current_phase` para gatear la ventana de invocación a `CLIMAX` (Reliquias Regla 7 / AC-RB24/RB25). Consume la fase, no la posee ni la re-deriva | ✅ Confirmado — reconciliación cross-GDD 2026-08-21 (cierra parte de Reliquias OQ-RB3) | +| UI/HUD | Dura | Consume `current_phase`, `time_remaining_to_climax_s` e `imminent_local_progress` para el temporizador visible y la escalada de aviso | Sin GDD (contrato propuesto) | +| Audio | Dura | Consume las señales de fase e `imminent_local_progress` para el crescendo prep→clímax | Sin GDD (contrato propuesto) | +| Guardado/Persistencia | Dura (bidireccional) | Persiste `elapsed_s` y la fase actual; al recargar, el reloj reanuda y la fase se recomputa vía F3 | ✅ Diseñado — contrato propuesto por este lado, a confirmar en el GDD de Guardado | + +**Nota de consistencia bidireccional (resuelta)**: el Sistema de Héroes (Approved) originalmente tenía **OQ-3** afirmando que `kaiju_timer_remaining_ratio` lo proveía *Encuentro con Kaiju*. Este GDD reclamó la propiedad del ratio para el Temporizador (decisión de diseño: un único reloj visible, una sola fuente de verdad). **Resuelto**: Héroes OQ-3 ya fue actualizado (2026-08-07) para atribuir el ratio a este sistema; Encuentro con Kaiju queda como consumidor paralelo, no dueño. + +**Nota (actualizada 2026-08-21)**: Encuentro con Kaiju (Approved), UI/HUD (Approved) y Reliquias/Bendiciones ya tienen GDD; sus contratos están confirmados. Solo **Audio** sigue sin GDD (Not Started) — su interfaz es contrato propuesto. Datos de Era/Civilización, Sistema de Héroes y Guardado/Persistencia ya existen. + +## Tuning Knobs + +| Knob | Símbolo | Tipo | Default | Rango seguro | Qué se rompe en extremos | +|---|---|---|---|---|---| +| Duración del acercamiento | `approach_duration_s` | float (s, config era) | 600 (10 min) | 300–1800 (5–30 min) | <300: la Preparación se siente apresurada, sin tiempo para 1-2 ciclos de recursos/posicionamiento — el Pilar 3 pierde su fase de "calma"; >1800: la Preparación se alarga sin tensión, el dread se diluye, contradice el "clímax explosivo" | +| Ventana de aviso | `imminent_window_s` | float (s, config era) | 45 | 30–90 (y siempre < 20% de `approach_duration_s`) | <30: no cabe la ventana de decisión de ~30s por héroe (Héroes AC-H29) — el aviso es demasiado corto para reaccionar; >90 (o >20% del acercamiento): `IMMINENT` se vuelve una segunda fase de calma en vez de "recta final", el aviso pierde urgencia | + +**Interacción entre knobs**: `imminent_window_s` es relativa a `approach_duration_s` — la guía de diseño "< 20%" evita que la recta final canibalice la fase de calma. Cambiar `approach_duration_s` sin revisar `imminent_window_s` puede violar esa proporción. + +**Enforcement** (coherente con la política loud-fail del proyecto — Héroes, Datos de Era con datos rotos): +1. **Constraints estructurales, loud-fail al cargar** (no se clampan — producirían división por cero o fases inválidas): `approach_duration_s > 0` y `0 < imminent_window_s < approach_duration_s`. Ver Edge Cases y AC. +2. **Rangos seguros de feel** (`approach_duration_s ∈ [300,1800]`, `imminent_window_s ∈ [30,90]`): fuera de rango → loud-fail en builds de desarrollo, para que un valor de era mal autorado se detecte al cargar y no produzca un pacing roto silenciosamente. +3. La proporción `imminent_window_s < 0.20 × approach_duration_s` es una **guía de diseño advisory** (no loud-fail): se recomienda pero no bloquea, porque es una preferencia de feel, no una condición de correctitud. + +**Constantes consumidas** (no se duplican): ninguna del registro global — la configuración de timing es propiedad de `EraDefinition` (Datos de Era/Civilización), leída por era. + +## Visual/Audio Requirements + +*Contrato provisional — sin audio bible ni dirección de audio aún (mismo estatus que el audio del Sistema de Héroes); re-verificar cuando existan.* + +**VA-1 — Crescendo prep→clímax (audio, el requisito principal)** +La escalada musical/ambiental es dirigida por `kaiju_timer_remaining_ratio` (continuo, durante toda `PREPARATION`) e `imminent_local_progress` (continuo, solo durante `IMMINENT`): `PREPARATION` **no** es un lecho estático — es una intensificación lenta y continua mapeada al descenso de `kaiju_timer_remaining_ratio` (1.0 → `1 − imminent_window_s/approach_duration_s`), deliberadamente sutil/sub-umbral (para no competir con la lectura táctica) pero perceptible si el jugador se detiene a escuchar; `IMMINENT` = rampa de tensión marcadamente más pronunciada (mapeada a `imminent_local_progress` 0→1 — el "cambio de marcha" evidente); `CLIMAX` = intensidad plena. Dirección de Música puede anclar un beat discreto de mitad de recorrido (p. ej. al cruzar `kaiju_timer_remaining_ratio≈0.5`) sobre esta misma señal continua como parte de su propia composición — no requiere una señal nueva de este sistema. Es el paralelo sonoro del Pilar 3 (calma **tensa** → estallido, no calma plana → estallido). El *timing* lo provee este sistema (señales de fase + ratio continuo); la composición pertenece a Dirección de Música. + +**VA-2 — Cues discretos de cambio de fase (canal periférico)** +`entered_imminent` y `entered_climax` disparan cada uno un cue audible discreto, para que el jugador registre la transición aunque no esté mirando el temporizador (el sonido es el mejor canal periférico en un RTS ocupado — mismo principio que Héroes A-4). Refuerza la regla de la fantasía: informar, nunca emboscar. + +**VA-3 — Visual mínimo propio** +Este sistema no posee VFX de mundo. Su representación visual es UI (ver UI Requirements). Cualquier telegrafío visual de "el kaiju se acerca" (silueta creciente en el horizonte) pertenece a la presentación de Encuentro con Kaiju, aunque puede ser *disparado* por las señales de fase de este sistema. La UI del temporizador usa la paleta apagada base (art bible 4.1); la urgencia de `IMMINENT` nunca se comunica solo por color (accesibilidad, art bible 4.5 — ver UI Requirements). + +## UI Requirements + +*Referencia rectora: Art Bible Sección 7 (UI 100% diegética). El diseño de pantalla concreto pertenece a UI/HUD; esta sección fija el contrato de qué debe comunicar.* + +**U-1 — Temporizador visible durante todo encuentro activo** +Mientras la era está `ACTIVE`, la UI muestra en todo momento la fase actual y una representación del tiempo restante al clímax (`time_remaining_to_climax_s`). La cuenta atrás es siempre legible (regla de la fantasía). + +**U-2 — Escalada de `IMMINENT` multi-canal (no solo color)** +Al entrar a `IMMINENT`, la UI escala visiblemente (dirigida por `imminent_local_progress`) para comunicar "recta final". La urgencia se señaliza por **forma/movimiento + color como refuerzo terciario**, nunca solo por color (art bible 4.5, mismo patrón multi-canal que Héroes U-1/U-2, por accesibilidad de daltonismo). + +**U-3 — Las 3 fases son distinguibles de un vistazo** +`PREPARATION`, `IMMINENT` y `CLIMAX` se leen como estados visualmente distintos; la transición a `CLIMAX` es inequívoca. + +**U-4 — Forma diegética, no reloj digital** +La representación sigue el estilo diegético del art bible (Sección 7) — un motivo ritual (p. ej. un recipiente que se vacía, un arco que se consume, una silueta que se acerca), no un cronómetro numérico estéril. La **forma concreta es una decisión de UX** (ver Open Questions / UX Flag). + +> **📌 UX Flag**: Este sistema tiene requisitos de UI (cuenta atrás visible + escalada de `IMMINENT`). En Pre-Producción, correr `/ux-design` para la forma diegética del temporizador **antes** de escribir epics — las stories deben citar el spec de UX, no este GDD directamente. + +## Acceptance Criteria + +*Convención: cada AC usa formato Dado/Cuando/Entonces y una etiqueta de tipo de evidencia (`[Logic/unit]`, `[Integration]`) según la taxonomía de evidencia de testing del proyecto. Los AC marcados **(bloqueado)** dependen de un sistema sin GDD aún (Encuentro con Kaiju, UI/HUD) — hoy solo verificables mediante un mock del contrato en el límite de este sistema; deben re-verificarse contra el GDD real cuando exista. Salvo que se indique lo contrario, todos los valores numéricos de ejemplo usan los defaults MVP: `approach_duration_s=600`, `imminent_window_s=45` → `imminent_threshold_s=555`.* + +### A. Reglas Core (1-7) + +**AC-T01 — Carga de timing data-driven por era (Regla 1)** **[Logic/unit]** +Dado un `EraDefinition` con `approach_duration_s` e `imminent_window_s` poblados, cuando el temporizador se inicializa para la era `ACTIVE`, entonces lee ambos valores exclusivamente desde ese resource — ningún valor de timing está hardcodeado en el código del sistema (verificable por inspección: cambiar los valores del `.tres` cambia el comportamiento sin tocar código). + +**AC-T02 — Un solo reloj, fases como umbrales (Regla 2)** **[Logic/unit]** +Dado el temporizador corriendo, cuando `elapsed_s` cruza el umbral que separa dos fases (p. ej. `PREPARATION→IMMINENT`), entonces `elapsed_s` es continuo a través del cruce (no se resetea a 0 ni salta) — confirma que las fases son umbrales sobre un único reloj, no relojes independientes reiniciados por fase. + +**AC-T03 — `kaiju_timer_remaining_ratio` correcto y expuesto (Regla 3, valor)** **[Logic/unit]** +Dado `time_remaining_to_climax_s=570` y `approach_duration_s=600`, cuando se lee `kaiju_timer_remaining_ratio`, entonces el valor es `0.95` (dentro de tolerancia ±0.0001) y es consultable públicamente en cualquier frame mientras la era está `ACTIVE`. + +**AC-T03b — Encuentro con Kaiju consume, no posee (Regla 3, contrato)** **(desbloqueado — Encuentro con Kaiju Approved)** **[Integration]** +*Contrato real (ya no mock): Encuentro con Kaiju (Approved) confirma explícitamente el consumidor read-only — su AC-K04 audita que "solo lee `entered_climax` y `kaiju_timer_remaining_ratio`, nunca escribe de vuelta", y su Dependencies declara "nunca escribe de vuelta hacia Temporizador". El GDD real **no** introdujo ninguna vía de escritura (no hay pausa de reloj por cinemática), así que la asunción del mock se sostiene — verificable sin mock contra Encuentro AC-K04.* +Dado Encuentro con Kaiju suscrito a las señales de fase, cuando el ratio cambia en este sistema, entonces solo lee el valor — ninguna llamada modifica `kaiju_timer_remaining_ratio` ni el `elapsed_s` interno de este sistema (cross-check contra Encuentro AC-K04). + +**AC-T04 — Puramente temporizado: sin aceleración/pausa/reversa desde gameplay (Regla 4a)** **[Logic/unit]** +Dado el temporizador corriendo en `PREPARATION` o `IMMINENT`, cuando se invoca cualquier acción de gameplay disponible al jugador (mover tropas, invocar recursos, comandos de héroe), entonces ninguna de esas acciones altera `elapsed_s` ni la fase — solo el paso del tiempo de juego lo hace. + +**AC-T04b — Monotonía de `elapsed_s` en juego activo (Regla 4b)** **[Logic/unit]** +Dado el temporizador corriendo sin pausa ni carga de partida, cuando se muestrean N lecturas sucesivas de `elapsed_s` a través de frames consecutivos, entonces cada lectura es siempre ≥ la anterior — `elapsed_s` nunca decrece durante juego activo. + +**AC-T05 — Legibilidad: fase y tiempo restante siempre consultables (Regla 5a)** **[Logic/unit]** +Dado el temporizador corriendo en cualquier fase mientras la era está `ACTIVE`, cuando se consulta `current_phase` y `time_remaining_to_climax_s`, entonces ambos valores están disponibles y son correctos según F1/F3 en ese mismo frame — nunca hay un frame donde estos valores sean nulos o estén desactualizados. + +**AC-T05b — Consumidor de UI/HUD recibe legibilidad en tiempo real (Regla 5b)** **(bloqueado — UI/HUD sin GDD)** **[Integration]** +*Mock Contract Assumptions: el mock de UI/HUD hace polling (o se suscribe a un signal de actualización) de `current_phase` y `time_remaining_to_climax_s` cada frame, sin caché ni retraso propio. Si el GDD real de UI/HUD introduce throttling de actualización, re-verificar el límite de legibilidad de esta AC.* +Dado un mock de UI/HUD consumiendo `current_phase`/`time_remaining_to_climax_s`, cuando la fase cambia, entonces el mock refleja el nuevo valor en el mismo frame del cambio — nunca se observa un HUD mostrando la fase anterior tras la transición. + +**AC-T06 — `CLIMAX` fija el ratio en 0 y lo sostiene (Regla 6)** **[Logic/unit]** +Dado el temporizador entrando a `CLIMAX` (elapsed_s=600), cuando transcurre tiempo de juego adicional dentro del combate del clímax (p. ej. 100s más), entonces `kaiju_timer_remaining_ratio` permanece exactamente en `0.0` durante todo ese intervalo — nunca vuelve a subir ni fluctúa. + +**AC-T07 — Contrato de superficie: solo pacing, sin combate (Regla 7a)** **[Logic/unit]** +Dado el temporizador instanciado en aislamiento (sin ningún sistema de combate, IA de kaiju, o cálculo de daño presente), cuando se ejecuta un ciclo completo `PREPARATION→IMMINENT→CLIMAX`, entonces el ciclo se completa sin error de dependencia faltante — la única superficie pública son las señales `entered_imminent`/`entered_climax` y los getters de fase/ratio/tiempo, sin métodos de instanciación, IA o daño. + +**AC-T07b — Consumidores solo usan la superficie documentada (Regla 7b)** **(parcial — Encuentro con Kaiju Approved; UI/Audio aún sin GDD)** **[Integration]** +*Contrato real (Kaiju) + mock (UI/Audio): Encuentro con Kaiju (Approved) solo consume `entered_climax` y `kaiju_timer_remaining_ratio` vía getter/suscripción (AC-K04, Dependencies) — dentro de la superficie documentada, sin API adicional. Los mocks de UI y Audio (aún sin GDD) siguen asumiendo que solo invocan los getters documentados (`current_phase`, `time_remaining_to_climax_s`, `kaiju_timer_remaining_ratio`, `imminent_local_progress`) y se suscriben a `entered_imminent`/`entered_climax`. Re-verificar el contrato de superficie cuando UI/Audio reciban GDD.* +Dado mocks de Encuentro con Kaiju, UI y Audio conectados a este sistema, cuando se ejecuta un ciclo completo del encuentro, entonces ningún mock invoca un método fuera de la superficie pública documentada. + +### B. Fórmulas (F1-F4) + +**AC-T08 — F1 exacto** **[Logic/unit]** +Dado `approach_duration_s=600` y `elapsed_s=30`, cuando se calcula `time_remaining_to_climax_s`, entonces el resultado es exactamente `570`. + +**AC-T09 — F2 exacto y con invariante de rango** **[Logic/unit]** +Dado `time_remaining_to_climax_s=570` y `approach_duration_s=600`, cuando se calcula `kaiju_timer_remaining_ratio`, entonces el resultado es `0.95` (dentro de tolerancia ±0.0001); y para cualquier combinación válida de `elapsed_s ≥ 0` y `approach_duration_s > 0`, el resultado siempre cae en `[0.0, 1.0]` (nunca negativo, nunca >1). + +**AC-T10 — F3 exacto en rangos no-boundary** **[Logic/unit]** +Dado los defaults MVP, cuando se evalúa la fase para `elapsed_s=100`, `elapsed_s=580` y `elapsed_s=650`, entonces el resultado es `PREPARATION`, `IMMINENT` y `CLIMAX` respectivamente. + +**AC-T11 — F4 exacto** **[Logic/unit]** +Dado los defaults MVP y `elapsed_s=570` (dentro de `IMMINENT`, pues `555 ≤ 570 < 600`), cuando se calcula `imminent_local_progress`, entonces el resultado es `15/45 = 1/3` (≈`0.3333`, dentro de tolerancia ±0.0001 — 1/3 es una fracción periódica, no el literal truncado `0.3333` exacto). + +**AC-T11b — F4 monótono** **[Logic/unit]** +Dado los defaults MVP, cuando se calcula `imminent_local_progress` en `elapsed_s=555`, `elapsed_s=570` y `elapsed_s=590`, entonces los resultados son `0.0`, `1/3` (≈`0.3333`) y `7/9` (≈`0.7778`) respectivamente, todos dentro de tolerancia ±0.0001 — estrictamente creciente conforme `elapsed_s` avanza dentro de `IMMINENT`. + +**AC-T12 — Cross-check con Héroes (3 ejemplos canónicos)** **[Integration]** +Dado los defaults MVP, cuando se evalúan los tres puntos canónicos citados en este GDD, entonces (todos los ratios/T dentro de tolerancia ±0.0001): `elapsed_s=30` → `ratio=0.95` → Héroes `T=0.05`; `elapsed_s=570` → `ratio=0.05` → Héroes `T=0.95`; entrada a `CLIMAX` → `ratio=0.0` → Héroes `T=1.0` (valores exactos, sin tolerancia necesaria). Confirma que la salida de F2 de este sistema alimenta correctamente la Fórmula H4 (ya Approved) del Sistema de Héroes sin necesidad de mock — Héroes es un sistema diseñado, no bloqueado. + +### C. Boundaries de Transición de Fase (F3, convención inclusive-lower) + +**AC-T13 — Boundary inferior exacto: `PREPARATION→IMMINENT`** **[Logic/unit]** +Dado los defaults MVP, cuando `elapsed_s == imminent_threshold_s` exactamente (`555`), entonces la fase ya es `IMMINENT` en ese mismo frame — no `PREPARATION`. + +**AC-T14 — Boundary superior exacto: `IMMINENT→CLIMAX`** **[Logic/unit]** +Dado los defaults MVP, cuando `elapsed_s == approach_duration_s` exactamente (`600`), entonces la fase ya es `CLIMAX`, `time_remaining_to_climax_s=0` y `kaiju_timer_remaining_ratio=0.0` en ese mismo frame. + +**AC-T15 — Un instante antes del boundary inferior** **[Logic/unit]** +Dado los defaults MVP, cuando `elapsed_s=554.999`, entonces la fase es aún `PREPARATION`. + +**AC-T16 — Un instante antes del boundary superior** **[Logic/unit]** +Dado los defaults MVP, cuando `elapsed_s=599.999`, entonces la fase es aún `IMMINENT` y `kaiju_timer_remaining_ratio` es un valor positivo pequeño (`> 0.0`), nunca `0.0` antes del boundary exacto. + +### D. Edge Cases + +**AC-T17 — Loud-fail: `approach_duration_s ≤ 0`** **[Logic/unit]** +Dado un `EraDefinition` con `approach_duration_s=0` o negativo, cuando el temporizador intenta inicializarse para esa era, entonces la carga falla ruidosamente (error/assert en build de desarrollo) y el reloj nunca corre con esa duración. + +**AC-T18 — Loud-fail: `imminent_window_s` fuera de `(0, approach_duration_s)`** **[Logic/unit]** +Dado un `EraDefinition` con `imminent_window_s ≥ approach_duration_s` (p. ej. igual) **o** con `imminent_window_s ≤ 0`, cuando el temporizador intenta inicializarse, entonces la carga falla ruidosamente en ambos casos — nunca se corre con un `imminent_threshold_s` fuera de rango. + +**AC-T19 — Orden de señales preservado ante un salto de delta grande** **[Logic/unit]** +Dado el temporizador en `PREPARATION` cerca del final de la fase, cuando un único paso de tiempo (delta grande, p. ej. tras un stall) avanza `elapsed_s` de un valor `< imminent_threshold_s` a un valor `≥ approach_duration_s` en un solo frame, entonces el sistema emite `entered_imminent` y luego `entered_climax`, en ese orden, dentro del mismo frame — nunca se omite `entered_imminent`. + +**AC-T20 — `elapsed_s` se congela al entrar a `CLIMAX`** **[Logic/unit]** +Dado el temporizador entrando a `CLIMAX` con `elapsed_s=600`, cuando transcurre tiempo de juego adicional, entonces el `elapsed_s` interno permanece en `600` (no crece sin límite) — verificable independientemente del invariante de clamp de F1/F2 (AC-T09), que ya cubre el caso de que no se congelara. + +**AC-T21 — Independencia de framerate (tiempo de juego, no tiempo real)** **[Logic/unit]** +Dado dos simulaciones que acumulan el mismo total de tiempo de juego (1.0s) mediante pasos de tamaño distinto (p. ej. 60 pasos de 1/60s vs. 30 pasos de 1/30s), cuando se compara `elapsed_s`, fase y `kaiju_timer_remaining_ratio` resultantes entre ambas simulaciones, entonces los tres valores son idénticos — el pacing no depende del framerate. + +**AC-T22 — Pausa global (menú, `SceneTree.paused`) congela `elapsed_s`** **[Logic/unit]** +Dado el temporizador corriendo, cuando el jugador abre el menú de pausa global y transcurre tiempo real mientras está pausado, entonces `elapsed_s` no avanza; al despausar, el reloj reanuda exactamente desde el valor que tenía al pausar. *(Régimen ortogonal al de TimeControl: el menú usa `SceneTree.paused`; la pausa/cámara-lenta de tiempo de juego llega vía `TimeControl.game_tick`/`time_scale` — ver AC-T21 y las reglas del reloj. Ambos congelan `elapsed_s`, por vías distintas.)* + +**AC-T23 — Guardado/carga recomputa la fase desde `elapsed_s` vía F3 (no confía en fase almacenada)** **[Logic/unit]** +Dado un estado guardado con `elapsed_s=570` pero un campo de fase almacenado deliberadamente desincronizado (p. ej. `PREPARATION` en vez de `IMMINENT`), cuando se recarga la partida, entonces la fase efectiva tras la carga es `IMMINENT` (recomputada vía F3 desde `elapsed_s=570`), ignorando el valor de fase persistido incorrecto. + +**AC-T24 — Guard de ratio antes de que la era esté `ACTIVE`** **[Logic/unit]** +Dado una era que aún no está `ACTIVE` (reloj no iniciado), cuando se consulta `kaiju_timer_remaining_ratio` o `current_phase`, entonces el sistema no retorna un valor que sugiera un encuentro en curso (p. ej. `1.0`/`PREPARATION` engañoso) — retorna un estado "no activo" explícito y documentado. + +**AC-T25 — `CLIMAX` persiste indefinidamente sin evaluación externa de fin de era** **[Logic/unit]** +Dado el temporizador en `CLIMAX`, cuando transcurre un período de tiempo de juego arbitrariamente largo sin que ningún sistema externo (Encuentro con Kaiju, Transición de Era) llame a evaluar el fin del encuentro, entonces la fase permanece `CLIMAX` y el ratio permanece `0.0` indefinidamente — este sistema nunca transiciona por sí mismo fuera de `CLIMAX`. + +### E. Tuning Knobs — Enforcement + +*Los constraints estructurales (`approach_duration_s > 0`, `0 < imminent_window_s < approach_duration_s`) ya están cubiertos por AC-T17/AC-T18 arriba. Esta sección cubre los dos niveles adicionales de enforcement descritos en Tuning Knobs: rango seguro de feel y la proporción advisory.* + +**AC-T26 — Loud-fail en dev por rango de feel, aun con constraint estructural satisfecho** **[Logic/unit]** +Dado un `EraDefinition` con `approach_duration_s=100` (estructuralmente válido, `>0`, pero fuera del rango seguro `[300,1800]`) **o** con `imminent_window_s=200` (estructuralmente válido, `0 < 200 < approach_duration_s`, pero fuera del rango seguro `[30,90]`), cuando el temporizador intenta inicializarse en un build de desarrollo, entonces la carga falla ruidosamente en ambos casos — un valor mal autorado se detecta al cargar, no en runtime silencioso. + +**AC-T27 — La proporción <20% es advisory, no bloqueante (confirma que SÍ carga)** **[Logic/unit]** +Dado un `EraDefinition` con `approach_duration_s=300` e `imminent_window_s=90` (ambos individualmente dentro de sus rangos seguros, pero `90/300=30% > 20%`, violando la guía de proporción), cuando el temporizador se inicializa en un build de desarrollo, entonces la carga **se completa sin error** — confirma que la guía "<20%" es una recomendación de diseño, no una condición de correctitud que bloquee la carga (distinto de AC-T26). + +### Clasificación de tipo de story (para el gate de evidencia) + +| AC # | Tipo | Evidencia requerida | Nivel de gate | +|---|---|---|---| +| AC-T01, AC-T02, AC-T03, AC-T04, AC-T04b, AC-T05, AC-T06, AC-T07 | Logic | Unit test automatizado | Bloqueante | +| AC-T08 a AC-T27 (excepto AC-T12) | Logic | Unit test automatizado | Bloqueante | +| AC-T03b, AC-T05b, AC-T07b | Integration (bloqueado) | Integration test con mock de contrato; re-verificar contra GDD real | Bloqueante (hoy verificable solo vía mock) | +| AC-T12 | Integration | Integration test (Héroes ya diseñado, sin mock necesario) | Bloqueante | + +### Huecos/bloqueados marcados (no diferidos silenciosamente) + +1. **AC-T03b** — ✅ desbloqueado (2026-08-12): Encuentro con Kaiju Approved confirma el consumo read-only (AC-K04); verificable sin mock. **AC-T07b** parcial: lado Kaiju resuelto, sigue bloqueado en UI/Audio (sin GDD). +2. **AC-T05b** — la legibilidad en tiempo real hacia UI/HUD depende del GDD real de UI/HUD (incluyendo si introduce throttling de actualización). Re-verificar cuando exista. +3. **AC-T07b** — el contrato de "solo superficie documentada" para los tres consumidores (Kaiju, UI, Audio) es hoy un mock conjunto; cualquiera de los tres GDDs reales podría requerir una API no contemplada aquí. Re-verificar cuando existan. +4. **VA-1..3 y U-1..4 (Visual/Audio, UI Requirements)** — deliberadamente sin AC en este documento: son contratos provisionales sin GDD propietario (UI/HUD, Audio). Recomendación: sus ACs se escriben en los futuros GDDs de UI/HUD y Audio, citando este sistema como fuente de señales/ratio — no aquí, para evitar fijar un contrato de UI/Audio antes de que exista su propio diseño (ver UX Flag en este GDD). + +## Open Questions + +| # | Pregunta | Owner | Resolución objetivo | +|---|---|---|---| +| OQ-1 | ¿Se añade un trigger anticipado del clímax (el jugador provoca al kaiju antes)? Descartado para el MVP (puramente temporizado); revisitar si el playtest muestra que los jugadores quieren provocar el clímax temprano | game-designer | Playtest del MVP | +| OQ-2 | ~~¿Quién evalúa victoria/derrota al final del `CLIMAX`?~~ ✅ **RESUELTO 2026-08-12**: Encuentro con Kaiju (Approved) es el dueño (Regla 9/AC-K49–K53) — declara victoria al morir el kaiju y derrota al morir el último héroe. Este sistema sigue llevando el encuentro solo *hasta* el clímax | game-designer | ✅ Cerrado (Encuentro Regla 9) | +| OQ-3 | Forma diegética concreta del temporizador visible (recipiente/arco/silueta/motivo) | ux-designer | Al diseñar UI/HUD (`/ux-design`) | +| OQ-4 | ~~¿El `CLIMAX` necesita su propio timer interno (duración de pelea / enrage)?~~ ✅ **RESUELTO 2026-08-12**: sí, y vive en Encuentro con Kaiju (Approved) — la Furia (`ENRAGED`, Regla 10/F5) dispara por tiempo (`enrage_time_threshold_s`) o por desenganche de roster, acotando la escalada del clímax. La cota dura *total* se aceptó como riesgo documentado post-MVP (Encuentro OQ-12) | systems-designer / game-designer | ✅ Cerrado (Encuentro Regla 10) | +| OQ-5 | ~~Confirmar que Encuentro con Kaiju **consume** (no posee) `kaiju_timer_remaining_ratio`~~ ✅ **RESUELTO 2026-08-12**: Encuentro con Kaiju (Approved) lo consume read-only (AC-K04, Dependencies); la propiedad es de este sistema. Confirmado en AC-T03b | game-designer | ✅ Cerrado (Encuentro AC-K04) | diff --git a/design/gdd/ui-hud.md b/design/gdd/ui-hud.md new file mode 100644 index 0000000000..a2fe043bfa --- /dev/null +++ b/design/gdd/ui-hud.md @@ -0,0 +1,335 @@ +# UI/HUD + +> **Status**: Approved (design-review 2026-08-14 — blocking items 1–3 aplicados y aceptados) +> **Author**: user + agents (systems-designer, art-director, qa-lead) +> **Last Updated**: 2026-08-14 +> **Implements Pillar**: Pilar 4 (Historia Jugada, No Contada); soporte a Pilar 1 (Sacrificio con Peso) y Pilar 3 (Ritual→Clímax) + +## Overview + +UI/HUD es la capa de presentación que convierte el estado crudo expuesto por Combate/Daño, Sistema de Héroes, Control y Selección de Unidades, Temporizador de Preparación/Ritual y Encuentro con Kaiju en señales legibles para el jugador durante una sesión activa: vida y estado de combate, selección y prioridad héroe/tropa, la cuenta atrás ritual hacia el clímax, el veredicto emergente de la muerte de un héroe, y las advertencias de telegrafío/marcado del kaiju. No posee ningún dato de juego propio — solo lee eventos y estado expuestos por otros sistemas, y decide cómo, cuándo y con qué intensidad mostrarlos. Para el jugador, es el sistema que hace posible el Pilar 4 (Historia Jugada, No Contada) sin una sola línea de texto editorializante: cada decisión táctica (a quién proteger, cuándo sacrificar, cuándo retirarse) se vuelve visible a través de forma, movimiento y color — nunca a través de un cartel que le diga al jugador qué sentir. El alcance de este GDD es la superposición HUD durante una sesión activa (preparación + clímax); los menús (pausa, ajustes, resultado, panteón, transición de era) quedan fuera y se diseñarán en sistemas o pasadas de UI posteriores. + +## Player Fantasy + +> **Framing**: Directo — el jugador interactúa con UI/HUD en cada instante de la sesión activa; es su ventana perceptual constante al estado del combate y del ritual. + +La fantasía que sirve UI/HUD es la de un comandante que **siempre sabe la verdad exacta del campo de batalla, sin que nadie se la suavice**. No hay iconos de "¡Peligro!" ni "¡Victoria!" que le digan al jugador qué sentir — solo formas, movimiento y color que comunican con precisión fría cuánto tiempo de vida le queda a un héroe, qué tan cerca está el clímax, y qué reliquia está naciendo de una muerte. Es la claridad de un HUD de Warcraft 3 (barras de vida legibles a simple vista en un enjambre de unidades) combinada con la honestidad emocional de los "boons" de Hades (un ícono nunca miente sobre lo que hace) — pero aplicada a apuestas permanentes: cuando el HUD le dice al jugador que un héroe está en la ventana de ~30 segundos antes de morir, esa lectura es literal, no drama de UI. + +Esto sirve directamente al **Pilar 1 (Sacrificio con Peso)** — la UI es el instrumento que hace la apuesta legible en tiempo real — y al **Pilar 4 (Historia Jugada, No Contada)** — el test de diseño de la UI es: si un jugador puede sentir la pérdida de un aliado como un momento importante *solo* por lo que ve en pantalla, sin ningún texto que se lo explique, el sistema cumplió su propósito. + +*Test de diseño de este sistema*: si una señal de la UI necesita texto para comunicar su urgencia o significado, el diseño visual falló y debe rehacerse con forma/movimiento/color antes de añadir texto. + +*(Autoría en modo lean reservó especialistas para D/H; el `/design-review` completo del 2026-08-14 sí consultó `creative-director` (síntesis senior) + game-designer/ux-designer sobre esta sección — ver review-log.)* + +## Detailed Design + +*(Autoría en modo lean no spawneó especialistas aquí; el `/design-review` completo del 2026-08-14 consultó game-designer, ux-designer, ui-programmer y godot-specialist sobre esta sección — hallazgos aplicados: Core Rule 6b, 11 mouse_filter, tabla de estados. Ver review-log.)* + +### Core Rules + +1. **UI/HUD es puramente de lectura (read-only)** — nunca escribe estado de vuelta a ningún sistema que consume, con la única excepción de la retroalimentación visual de Sacrificio (Regla 7), que es un contrato provisional de solo-feedback, no de autoridad de comando. +2. **Composición de regiones**: panel inferior (comando/selección), timer superior-centro (fase ritual), overlays de mundo anclados a entidades que se mueven con la cámara (barras de vida, ancla del readout de veredicto del héroe, marcas/anillo del kaiju), viñeta de pantalla periférica (aviso de telegrafío). Sin minimapa en MVP. +3. **Barras de vida**: visibles solo si la unidad está seleccionada O por debajo de vida máxima (resuelve CV-5 de Combate/Daño — decisión tomada en este GDD). Usan la misma paleta Hueso→Sangre Vieja que el flash de impacto (CU-1). +4. **Selección**: cada unidad seleccionada recibe un contorno (Acero Violeta-Ceniza, solo contorno, sin relleno) derivado del selection set de Control y Selección de Unidades; cuando una selección resuelve a un héroe sobre una tropa superpuesta, el marcador del héroe usa grafía de forma distinta, no solo tamaño (resuelve U-6/AC-H33). +5. **Timer ritual**: forma diegética (vasija que se drena / arco que se consume), nunca un reloj digital. Las 3 fases (PREPARATION/IMMINENT/CLIMAX) son distinguibles a simple vista; la escalada de IMMINENT es multi-canal (forma/movimiento primero, color solo terciario). +6. **Readout de veredicto del héroe** (implementa U-1..U-4 de Sistema de Héroes): visible solo si el héroe está en riesgo activo — gateado por `time_to_death`/`incoming_damage_rate_total`, nunca por HP% — o está seleccionado. Anclado al elemento asimétrico del héroe. 3 estados discretos de tier (Delgada/Sólida/Legendaria) diferenciados por forma, no por gradiente de color. Se congela al entrar `DYING`. El gate de riesgo tiene **dos caminos** (F1): el camino normal con histéresis (`T_show`+debounce) y un **camino de disparo instantáneo** que salta `T_show` y el debounce cuando `danger_time_s < T_instant`=5.0s — de modo que una muerte por ráfaga (héroe a vida plena eliminado dentro de la ventana de debounce de 0.5s) nunca ocurre sin readout. El camino instantáneo garantiza el compromiso de "informar, nunca emboscar" del Pilar 4. +6b. **Director de atención de muerte de héroe** (resuelve el hueco de muerte silenciosa detectado en review): cuando un héroe cruza a `AT_RISK` (vía cualquiera de los dos caminos de F1) o a `DYING` **mientras está fuera de pantalla o no seleccionado**, UI/HUD dispara una señal redundante multi-canal para que el jugador nunca pierda el beat de permadeath — la única excepción deliberada a la regla de "sin indicadores de fuera de pantalla" (justificada por las apuestas de permadeath, Pilar 1). Tres canales simultáneos: **(a)** un marcador direccional de borde de pantalla que apunta hacia el héroe endangered (solo para `AT_RISK`/`DYING`, no para daño rutinario), reproyectado/clamped al borde vía F2; **(b)** un sting de audio breve + pulso háptico en la transición; **(c)** un empujón de cámara (hint de auto-paneo o bloom de borde) hacia el héroe al entrar `DYING`. Los tres respetan la regla de ≥3 canales redundantes y no editorializan (Regla 10). El marcador de borde y el empujón de cámara cesan en cuanto el héroe entra al viewport o se confirma `DEAD`. +7. **Retroalimentación de Sacrificio (contrato PROVISIONAL — ver Open Questions)**: al emitir la orden, el cursor/reticle cambia a una forma distinta de la de mover/atacar mientras la orden está pendiente; al aceptarse, la silueta del héroe recibe un pulso de confirmación de un frame. UI/HUD solo posee esta retroalimentación visual — el verbo de entrada y el gate de `D` siguen siendo de Control/Combate (OQ-2 en ambos, sin resolver). +8. **Telegrafío del kaiju**: los 3 tercios explícitos sobre `telegraph_duration_s`=14.0s en el cuerpo del kaiju son posesión de Encuentro con Kaiju (fuera de este GDD) — pero el canal periférico no-audio (viñeta de borde de pantalla + rumble de gamepad) desde el tercio 1, escalando con el mismo ritmo, es posesión de UI/HUD. Tope de 3 flashes/seg (fotosensibilidad); un toggle de flash reducido sustituye la pulsación por una rampa continua de brillo. +9. **Marcas del kaiju**: el anillo secundario de `guard_radius`=4.0 alrededor de `marked_target` (vía defensiva, KV-2) es renderizado por UI/HUD; la runa de suelo en sí y la lectura de `marked_threat` en el cuerpo del kaiju (KV-3) son posesión visual de Encuentro con Kaiju/art-director. +10. **Sin texto editorializante en ningún elemento** (resuelve CU-4/U-5/KU-3) — solo estado/números cuando corresponda, nunca frases de sentimiento ("¡Peligro!", "¡Muerte heroica!"). +11. **Ningún elemento de HUD acepta input directo**, salvo el cursor virtual de gamepad (cuyo comportamiento de snap magnético pertenece a Control — UI/HUD solo dibuja su apariencia). UI/HUD es estrictamente de salida. **Mecanismo (Godot 4.6)**: los overlays anclados a mundo se implementan como nodos `Control` dentro de un `CanvasLayer` (posicionados por-frame vía F2), y **cada uno debe fijar `mouse_filter = MOUSE_FILTER_IGNORE`** — de lo contrario consumirían silenciosamente los clics de selección de unidad destinados a las unidades por debajo (Godot 4.6 separa el foco mouse/touch del de teclado/gamepad; un `Control` con filtro por defecto captura el clic). AC-U28 verifica el síntoma; esta regla fija el mecanismo. + +### States and Transitions + +| Elemento | Transición | Disparador | +|---|---|---| +| Barra de vida | `HIDDEN → VISIBLE` | unidad seleccionada O `current_health < max_health` | +| Barra de vida | `VISIBLE → HIDDEN` | unidad deseleccionada Y `current_health == max_health` | +| Readout de veredicto del héroe | `HIDDEN → AT_RISK` (camino histéresis) | `danger_time_s ≤ T_show`=30.0s sostenido ≥`debounce_on_s`=0.5s (F1) | +| Readout de veredicto del héroe | `HIDDEN → AT_RISK` (camino instantáneo) | `danger_time_s < T_instant`=5.0s — salta `T_show` y debounce (F1, muerte por ráfaga) | +| Readout de veredicto del héroe | `HIDDEN/AT_RISK → SELECTED_VIEW` | héroe seleccionado por el jugador (visible independientemente del riesgo) | +| Readout de veredicto del héroe | `AT_RISK/SELECTED_VIEW → FROZEN` | héroe entra a `DYING` (snapshot D/T/P, deja de actualizar) | +| Readout de veredicto del héroe | `HIDDEN → FROZEN` (muerte súbita, nunca renderizado) | héroe entra a `DYING` sin haber cruzado F1 ni estar seleccionado — se toma el snapshot D/T/P igual; se renderiza solo si el jugador lo trae a vista antes de `DEAD` (ver Edge Cases) | +| Readout de veredicto del héroe | `FROZEN → HIDDEN` | héroe confirmado `DEAD` (Permadeath resuelve el beat) | +| Director de atención (marcador de borde + cámara) | `OFF → ACTIVE` | héroe cruza a `AT_RISK`/`DYING` **fuera de pantalla o no seleccionado** (Core Rule 6b) | +| Director de atención (marcador de borde + cámara) | `ACTIVE → OFF` | el héroe entra al viewport O se confirma `DEAD` | +| Timer ritual | `PREPARATION → IMMINENT → CLIMAX` | señales `entered_imminent`/`entered_climax` del Temporizador (consumidas, no re-derivadas) | +| Viñeta de telegrafío | `OFF → THIRD_1 → THIRD_2 → THIRD_3` | `telegraph_progress` cruza 0.33 / 0.66 / 1.0 (consumido de Encuentro con Kaiju, KU-1) | +| Viñeta de telegrafío | `THIRD_3 → OFF` | `STRIKING` resuelto O cancelado (razón expuesta por KU-1) | +| Feedback de Sacrificio | `IDLE → PENDING → CONFIRMED → IDLE` | orden emitida → aceptada por Combate/Control → siguiente frame vuelve a `IDLE` | + +### Interactions with Other Systems + +- **Combate/Daño → UI/HUD** (lee): `current_health`/`max_health` por unidad, estado de enganche (`NO_TARGET`/`TARGET_ACQUIRED`/`ATTACKING`), eventos de golpe (disparan flash de una silueta), tasa de daño entrante reciente. Nada fluye en sentido inverso. +- **Sistema de Héroes → UI/HUD** (lee): tier proyectado de `relic_quality` (mapeado vía U-1/U-2), snapshot D/T/P al entrar `DYING`, estado de prioridad de selección hero>tropa, `asymmetric_element`/`relic_category` (identidad visual). Nada fluye en sentido inverso. +- **Control y Selección de Unidades → UI/HUD** (lee): selection set activo, método de input activo (mouse vs gamepad — determina si el cursor virtual se dibuja), rectángulo de box-select durante el arrastre, posición del cursor virtual de gamepad. Nada fluye en sentido inverso — UI/HUD no decide selección, solo la dibuja. +- **Temporizador de Preparación/Ritual → UI/HUD** (lee): `current_phase`, `time_remaining_to_climax_s`, `imminent_local_progress`. +- **Encuentro con Kaiju → UI/HUD** (lee): estado del ciclo de intención (`SEEKING`/`TELEGRAPHING`/`STRIKING`/`STAGGERED`), flag `ENRAGED`, `telegraph_progress` `[0.0,1.0]`, identidad de `marked_target`/`marked_threat` + razón de cancelación de marca, `time_to_death` e `incoming_damage_rate_total` por héroe, `stagger_duration_s` restante, un único evento discreto de victoria/derrota (UI/HUD no posee ni renderiza la pantalla de resultado — solo puede mostrar un acuse de recibo breve antes del hand-off, per decisión de alcance de este GDD). +- **Reliquias/Bendiciones → UI/HUD** (contrato **confirmado — `reliquias-bendiciones.md` Approved 2026-08-18**): expone `invocation_charges_remaining` (RU-1), `can_invoke`/`first_invocation_available` (RU-2/RU-2b), `blessing_offer` 3-choose-1 con identidad glanceable + `band_headroom` (RU-3/RU-3b), y `active_blessings: list` con ticks de expiración (RU-4). **Nota de carga cognitiva:** el draft de invocación corre durante `CLIMAX` (en cámara lenta, `offering_time_scale`, no en pausa) — OQ-U8 (riesgo de overload del HUD de CLIMAX) debe **re-contar el draft** como stream activo adicional; el análisis original lo predata. +- **Datos de Era/Civilización → UI/HUD** (constantes registradas): `world_unit_scale`=48, `camera_pan_speed`=30, `zoom_min`=0.5, `zoom_max`=1.5 — usados junto con la inversa de `screen_to_world_transform` para posicionar overlays de mundo sobre unidades/kaiju. + +## Formulas + +*(`systems-designer` consultado — MANDATORY incluso en modo lean, Sección D es de alto riesgo de implementación.)* + +### F1 — `hero_risk_threshold` (resuelve Héroes OQ-6 + Encuentro con Kaiju OQ-14) + +The `hero_risk_threshold` formula is defined as: + +`hero_risk_state = (danger_time_s < T_instant) OR hysteresis(danger_time_s, T_show, T_hide, min_visible_s, debounce_on_s)` +`danger_time_s = min(time_to_death, hero_current_health / max(incoming_damage_rate_total, ε))` + +El primer término es el **camino de disparo instantáneo**: en cuanto `danger_time_s` cae por debajo de `T_instant`, el readout se enciende ese mismo frame, saltando `T_show` y el debounce. Esto cierra la ventana de "muerte por ráfaga" (un héroe eliminado más rápido que `debounce_on_s`=0.5s) y cubre caídas discontinuas de `time_to_death` (p. ej. un recálculo por `ENRAGED`, cuya tasa de cambio es propiedad de Encuentro con Kaiju — ver OQ-U7). El segundo término (histéresis) sigue gobernando el peligro sostenido no letal y su banda muerta anti-parpadeo. Una vez ON por cualquiera de los dos caminos, `min_visible_s` sigue aplicando antes de poder apagarse. + +**Variables:** +| Variable | Symbol | Type | Range | Description | +|----------|--------|------|-------|-------------| +| Proyección solo-kaiju | `time_to_death` | float | [18.5, ∞) s | De Encuentro con Kaiju (locked) | +| DPS todas las fuentes | `incoming_damage_rate_total` | float | [0.0, ∞) | De Encuentro con Kaiju (locked) | +| Vida actual del héroe | `hero_current_health` | float | (0, hero_max_health] | Stat en vivo | +| Guarda epsilon | `ε` | float | 0.001 (const) | Previene división por cero | +| Tiempo de peligro combinado | `danger_time_s` | float | [0, ∞) | Señal "segundos hasta la muerte" comensurable | +| Umbral de disparo instantáneo | `T_instant` | float | 5.0 s (const) | Salta `T_show`+debounce; cierra la ventana de muerte por ráfaga | +| Umbral de aparición | `T_show` | float | 30.0 s (const) | Ancla el objetivo de ~30s de Héroes | +| Umbral de desaparición | `T_hide` | float | 38.0 s (const) | ~27% sobre T_show; ancho de banda muerta | +| Duración mínima visible | `min_visible_s` | float | 4.0 s (const) | Una vez ON, permanece al menos esto | +| Ventana de debounce ON | `debounce_on_s` | float | 0.5 s (const) | La condición debe sostenerse antes de ON | +| Estado de salida | `hero_risk_state` | bool | {OFF, ON} | Controla visibilidad del readout U-3 | + +**Output Range:** booleano; a lo sumo un ciclo ON→OFF por ventana `min_visible_s + debounce` bajo oscilación de peor caso. +**Example:** `hero_current_health=340`, `incoming_damage_rate_total=52.0` → tiempo por HP = 6.54s. `time_to_death=26.0s`. `danger_time_s=min(26.0,6.54)=6.54s` → readout se enciende (sostenido 0.5s). Luego `danger_time_s` sube a 41s → se apaga (≥38s, y llevaba visible >4s). +**Nota de diseño:** `min()` en vez de promedio — así un enjambre rápido de esbirros no queda enmascarado por un timer de kaiju lento. El HP se divide por el DPS para volver `incoming_damage_rate_total` (una tasa) comensurable con `time_to_death` (un tiempo). + +### F2 — `world_to_screen_transform` (inversa formal de `screen_to_world_transform`) + +The `world_to_screen_transform` formula is defined as: + +`world_to_screen_transform = viewport_size_px / 2 + (world_position_world - camera_position_world) × (world_unit_scale / zoom)` + +**Variables:** +| Variable | Symbol | Type | Range | Description | +|----------|--------|------|-------|-------------| +| Posición objetivo | `world_position_world` | Vector2 | world units | Posición a proyectar (héroe/marcador) | +| Posición de cámara | `camera_position_world` | Vector2 | world units | Foco actual de la cámara | +| Tamaño de viewport | `viewport_size_px` | Vector2 | px | Dimensiones del viewport actual | +| Zoom | `zoom` | float | [0.5, 1.5] | Rango registrado (zoom_min/zoom_max) | +| Escala de unidad de mundo | `world_unit_scale` | float | 48 (const) | px por unidad de mundo a zoom 1.0 | +| Salida | `screen_pos_px` | Vector2 | (-∞, ∞) px | Coordenadas en píxeles, puede caer fuera del viewport | + +**Output Range:** sin acotar — un resultado válido puede caer fuera de `[0, viewport_size_px]` (héroe detrás de cámara o fuera de pantalla). Elementos que deben permanecer visibles fuera de pantalla (marcador direccional del director de atención, Core Rule 6b) aplican su propio clamp/reproyección al borde; esta fórmula solo da la proyección cruda. +**Example:** cámara en (100,50), zoom=1.0, viewport 1920×1080, objetivo en (120,50) → `screen_pos_px=(1920,540)` — un punto 20 unidades de mundo a la derecha de la cámara cae exactamente en el borde derecho del viewport. (Convención de `zoom` idéntica a la de Datos de Era/Civilización: `zoom`<1 acerca — a `zoom_min`=0.5 la escala es 48/0.5=**96 px/unidad**, sprites al doble de tamaño; `zoom_max`=1.5 aleja, 32 px/unidad, mayor densidad de unidades en pantalla.) +**Nota de implementación (Godot 4.6 — MANDATORIA, resuelve blocking item de review):** F2 es la *definición matemática* de la proyección, **no la vía de implementación**. La implementación NO debe re-derivar esta transformada a mano: debe leer la transformada de canvas que Godot usa realmente para renderizar, vía `CanvasItem.get_global_transform_with_canvas()` (o el `get_canvas_transform()` del viewport) y, para el centro de cámara, `Camera2D.get_screen_center_position()` — **no** el `.position` crudo del nodo. Motivo: con `position_smoothing_enabled` o drag margins activos (y `camera_pan_speed`=30 produce paneos rápidos), el centro renderizado *retrasa* la posición del nodo, y una fórmula manual haría que los overlays anclados a mundo **se deslicen respecto de sus entidades** durante el paneo. La transformada nativa también absorbe gratis el `content_scale_mode` del proyecto (canvas_items vs viewport) y el escalado DPI/resize; la fórmula manual no. El mundo usa **Camera2D** (2D puro; confirmado contra el framing de tiles de Datos de Era/Civilización). Sin término de rotación porque la cámara no rota en MVP — si se añade rotación de cámara, F2 manual se rompería; otra razón para deferir a la transformada nativa. AC-U26 debe testear contra la salida real de la transformada de Godot, no solo la aritmética de F2. + +### F3 — `telegraph_vignette_intensity` (canal periférico propio de UI/HUD, KV-1b) + +The `telegraph_vignette_intensity` formula is defined as: + +`telegraph_vignette_intensity = clamp(base(third) + amplitude × abs(sin(π × freq(third) × elapsed_in_third_s)), 0.0, 1.0)` (modo flash) +Modo flash-reducido sustituye el término oscilante por una rampa lineal: `clamp(base(third) + amplitude × progress_within_third, 0.0, 1.0)`. + +**El `clamp()` es obligatorio, no cosmético**: sin él, la salida excede 1.0 en el extremo del rango seguro del knob `amplitude`. En el tercio 3 (`base(2)`=0.75) con `amplitude`=0.35 (techo del rango seguro documentado), el pico oscilante da `0.75 + 0.35 = 1.10 > 1.0`. El clamp explícito garantiza el rango `[0.0,1.0]` que AC-U22 verifica; la garantía algebraica anterior solo se sostenía en `amplitude`=0.25 exacto. + +**Variables:** +| Variable | Symbol | Type | Range | Description | +|----------|--------|------|-------|-------------| +| Progreso crudo | `telegraph_progress` | float | [0.0, 1.0] | Leído de Encuentro con Kaiju | +| Índice de tercio | `third` | int | {0,1,2} | `floor(telegraph_progress × 3)`, clamped | +| Progreso dentro del tercio | `progress_within_third` | float | [0.0, 1.0) | `(telegraph_progress × 3) − third` | +| Tiempo transcurrido en el tercio | `elapsed_in_third_s` | float | [0, 4.667) s | `progress_within_third × (telegraph_duration_s/3)` | +| Base escalonada | `base(third)` | float | {0.25, 0.50, 0.75} | `0.25 + 0.25 × third` | +| Frecuencia de pulso | `freq(third)` | float | {1.0, 2.0, 3.0} Hz | `min(1.0 + third, 3.0)` — tope de fotosensibilidad (KV-10) | +| Amplitud de pulso | `amplitude` | float | 0.25 (const) | Altura de oscilación/rampa sobre la base | +| Salida | resultado | float | [0.0, 1.0] | Alpha de viñeta, clamped | + +**Output Range:** [0.0, 1.0], garantizado por el `clamp()` explícito; ambos modos comparten `base()` para preservar paridad de accesibilidad. +**Example (modo flash):** `telegraph_progress=0.50` → tercio 1, `elapsed_in_third_s≈2.33s`, `freq=2.0Hz` → `base=0.50`. Término oscilante = `0.25 × abs(sin(π × 2.0 × 2.33))` = `0.25 × abs(sin(14.66 rad))` = `0.25 × 0.866` = **0.217** → intensidad = `0.50 + 0.217` ≈ **0.72**. **Equivalente flash-reducido:** `0.50 + 0.25×0.50 = 0.625`, suave y sin parpadeo. *(Nota: el modo flash pulsa por encima del modo flash-reducido en la cresta del seno — ambos comparten la misma `base()` de piso, que es lo que preserva la paridad de accesibilidad; los picos difieren por diseño.)* +**Nota de diseño:** el salto escalonado de `base()` da identidad de "qué tercio" instantánea incluso antes de que se lea un pulso; `freq()` escala 1→2→3Hz, aterrizando exactamente en el techo de fotosensibilidad en el tercio final, nunca por encima. + +## Edge Cases + +*(Autoría en modo lean no spawneó especialistas aquí; el `/design-review` completo del 2026-08-14 (systems-designer, game-designer, ux-designer) revisó y amplió esta sección — añadidos: muerte por ráfaga, director fuera de pantalla, corrección de zoom F2, apilamiento N-simultáneo. Ver review-log.)* + +- **Si dos o más héroes están simultáneamente `AT_RISK`**: cada uno renderiza su propio readout de peligro, anclado independientemente a su propio héroe — la apuesta de cada héroe es individualmente legible (Pilar 1). **Pero la legibilidad *por-héroe* no garantiza legibilidad *de escena*** cuando 3+ readouts coinciden con el timer, la viñeta, los anillos, las barras y los contornos durante el CLIMAX (el momento de mayor densidad de señal). El apilamiento sin límite ni prioridad, más el coste de draw calls de N overlays independientes contra el presupuesto de <1000 (technical-preferences.md), es un riesgo real de sobrecarga sensorial y de rendimiento — se necesita una regla de priorización/agregación (p. ej. una tira de roster compacta que resuma múltiples héroes en riesgo) y un tope suave de overlays concurrentes. No resuelto numéricamente aquí; flageado como OQ-U8 para vertical slice. (La legibilidad bajo N readouts simultáneos también carece de AC — ver AC-U32.) +- **Si `incoming_damage_rate_total = 0`** (sin esbirros enganchados, kaiju no apunta a este héroe): el término HP/DPS de F1 resuelve, vía la guarda `ε`, a un valor efectivamente inalcanzable, así que `danger_time_s` cae en `time_to_death` solo — sin división por cero, sin parpadeo. +- **Muerte por ráfaga (héroe a vida plena eliminado dentro de la ventana de debounce)**: si el daño entrante proyecta la muerte en menos de `debounce_on_s`=0.5s, el camino de histéresis de F1 nunca alcanzaría a encender el readout — pero el **camino de disparo instantáneo** (`danger_time_s < T_instant`=5.0s) lo enciende ese mismo frame, sin esperar debounce. Ningún héroe muere sin readout por ser demasiado rápido. Esto también cubre una caída discontinua de `time_to_death` (recálculo por `ENRAGED`): en cuanto el valor recomputado cae bajo `T_instant`, dispara. +- **Si un héroe entra a `AT_RISK` o `DYING` fuera de pantalla o sin estar seleccionado**: (1) el readout se congela en el snapshot D/T/P igual, aunque nada se renderice — si el jugador luego selecciona ese héroe o lo trae a vista antes de que se confirme `DEAD`, el estado congelado se renderiza de inmediato, sin re-disparar el congelamiento; y (2) el **director de atención** (Core Rule 6b) dispara sus tres canales redundantes — marcador direccional de borde de pantalla, sting de audio + pulso háptico, y empujón de cámara — para que el beat de permadeath nunca ocurra en silencio total. El marcador y el empujón cesan al entrar el héroe al viewport o confirmarse `DEAD`. (Esto reemplaza el hueco anterior donde una muerte fuera de pantalla no tenía ninguna señal, dado que no hay minimapa en MVP y la viñeta F3 es solo del kaiju.) +- **Si la viñeta de `TELEGRAPHING` del kaiju (F3) y la escalada `IMMINENT` del timer ritual compitieran por el borde de pantalla**: no hay conflicto real — `IMMINENT` solo ocurre durante `PREPARATION` y `TELEGRAPHING` solo durante `CLIMAX`; las fases del Temporizador están bloqueadas a ocurrir estrictamente antes de `CLIMAX` (nunca concurrentes), así que solo un efecto de borde de pantalla está activo a la vez. +- **Si el jugador activa el toggle de flash reducido**: se aplica globalmente a todo elemento de HUD que parpadee (viñeta de telegrafío F3 Y la señal de escalada `IMMINENT`, si esa señal también pulsa) — no solo al telegrafío del kaiju. Un toggle por-elemento dejaría un hueco de fotosensibilidad. +- **Si una orden de Sacrificio pendiente (Core Rule 7, estado `PENDING`) es rechazada** por Combate/Control (target/D-gate no satisfecho): el reticle vuelve de `PENDING` a `IDLE` con una señal de rechazo distintiva de un frame (pulso de forma/color distinto al pulso de `CONFIRMED`) — nunca silencioso, pero nunca texto (Regla 10 sigue aplicando). +- **Si dos unidades seleccionadas (un héroe y una tropa superpuesta) se renderizan al footprint de sprite más pequeño** (`zoom_max`=1.5 — la vista alejada de encuadre de kaiju, donde `zoom`>1 produce 32 px/unidad, la máxima densidad de unidades y el sprite más pequeño en pantalla; **corregido en review** — la versión previa citaba `zoom_min`=0.5, pero por la convención de Datos de Era/Civilización `zoom`<1 *acerca* y produce el sprite más grande, no el más pequeño): la distinción de grafía de forma (Regla 4/U-6) debe seguir siendo legible a esa escala — el tamaño mínimo legible exacto es asunto de art-bible/technical-artist, flageado en Visual/Audio Requirements, no resuelto numéricamente aquí. +- **Si `game_time_paused` se vuelve verdadero** (p. ej. durante el beat `DEATH_HOLD` de Permadeath — contrato aún no formalizado en ningún GDD Approved): todas las animaciones de HUD dirigidas por datos (drenado de barras de vida, cuenta atrás del timer ritual, pulso de viñeta de telegrafío) se congelan en su lugar en vez de seguir animando contra un reloj de juego pausado — provisional, flageado en Open Questions pendiente del GDD de Permadeath. +- **Si el tier de `relic_quality` de un héroe cruza un límite** (p. ej. Sólida→Legendaria) mientras el readout ya está visible (`AT_RISK` o `SELECTED_VIEW`): la forma/marco se actualiza de inmediato al nuevo tier — sin animación de transición especificada más allá del cambio discreto instantáneo (U-2 de Héroes rechaza explícitamente un gradiente de color, y ninguna animación intra-tier está definida; Héroes OQ-8 ya flagea si se necesita una señal intra-banda como pregunta de playtest, no resuelta aquí). **Flag de playtest (añadido en review)**: un corte discreto instantáneo en un momento de peso narrativo (Sólida→Legendaria justo antes de una muerte) puede leerse como glitch en vez de como declaración deliberada; validar en playtest si necesita un breve destello de confirmación de un frame (no un gradiente) para leerse como intencional. Owner: `ux-designer` + `art-director`; agrupar con Héroes OQ-8. +- **Si `world_to_screen_transform` (F2) proyecta un marcador fuera del viewport actual** (héroe detrás de cámara, fuera de pantalla): el elemento anclado a mundo afectado (barra de vida, readout de peligro, anillo de `guard_radius`) simplemente no se dibuja ese frame — ningún indicador/flecha de fuera-de-pantalla está especificado en este GDD (sería una adición de nivel Discovery, fuera del alcance MVP por la decisión de "solo HUD en sesión activa"). + +## Dependencies + +**Cross-reference check** (bidireccionalidad contra los 5 GDDs de dependencia, confirmada vía grep): Combate/Daño, Control y Selección de Unidades, Temporizador de Preparación/Ritual y Encuentro con Kaiju **ya listan a UI/HUD** como dependiente hacia abajo (actualmente etiquetados "Sin GDD" — a des-flaggear en Fase 5). **Hueco encontrado**: la tabla de Dependencies de `sistema-de-heroes.md` no tenía fila para UI/HUD pese a poseer los UI Requirements U-1..U-7 que este GDD implementa directamente (Sección C, Core Rule 6) — corregido en esta sesión. + +**Dependencias hacia arriba (upstream):** + +| Sistema | Tipo | Interfaz | Estado | +|---|---|---|---| +| Combate/Daño | Dura | `current_health`/`max_health`, estado de enganche, eventos de golpe, tasa de daño entrante (CU-1/CU-2/CU-3) | ✅ Approved | +| Sistema de Héroes | Dura | Tier de `relic_quality`, snapshot D/T/P, prioridad de selección, `asymmetric_element`/`relic_category` (U-1..U-7) | ✅ Approved | +| Control y Selección de Unidades | Dura | Selection set, método de input activo, rectángulo de box-select, posición del cursor virtual | Designed | +| Input | Dura | Consume las acciones `ui_*` en contextos de menú/`BLESSING_SELECT`, `active_input_method` (iconos de prompt; NO para visibilidad del anillo de foco — ver `input.md` Regla Núcleo 3), e `input_lockout_active` (freeze de HUD durante `DEATH_HOLD`). La pantalla de remapeo de controles la posee UI/HUD y lee/escribe el InputMap que Input expone | Designed (`input.md`) — bidireccionalidad registrada 2026-08-19 | +| Temporizador de Preparación/Ritual | Dura | `current_phase`, `time_remaining_to_climax_s`, `imminent_local_progress` | ✅ Approved | +| Encuentro con Kaiju | Dura | Estado del ciclo de intención, `ENRAGED`, `telegraph_progress`, marcas + razón de cancelación, `time_to_death`, `incoming_damage_rate_total`, `STAGGERED`, evento de victoria/derrota | ✅ Approved | +| Datos de Era/Civilización | Dura | Constantes `world_unit_scale`=48, `camera_pan_speed`=30, `zoom_min`=0.5, `zoom_max`=1.5 | Designed | +| Reliquias/Bendiciones | Dura | Expone cargas (RU-1), `can_invoke`/onboarding (RU-2/2b), oferta 3-choose-1 con identidad glanceable + `band_headroom` (RU-3/3b), `active_blessings: list` (RU-4). El draft corre en `CLIMAX` en cámara lenta | ✅ Approved (`reliquias-bendiciones.md`) — desbloquea AC-U30/OQ-U4 | + +**Dependientes hacia abajo (downstream):** + +| Sistema | Tipo | Interfaz | +|---|---|---| +| Accesibilidad | Dura | Consume los toggles/contratos de accesibilidad que este GDD define (flash reducido global, ≥3 canales redundantes por daltonismo) como base sobre la que construye | + +**Alcance del contrato de accesibilidad (aclarado en review)**: lo que este GDD define y garantiza es el **toggle de flash reducido global** (F3 + escalada IMMINENT) y la **redundancia de ≥3 canales sin depender de color** (HU-10, verificado por AC-U29). Lo que este GDD **NO** define y queda como responsabilidad del sistema de Accesibilidad downstream: escala de UI / tamaño de texto, la elección concreta de la herramienta/método de simulación CVD y su barra de aprobación, y cualquier exposición a lector de pantalla. **Nota Godot 4.6 (godot-specialist)**: la integración AccessKit de 4.5 (exposición del árbol de `Control` a tecnología asistiva / lector de pantalla) es un concern downstream *separado y no relacionado* del contrato de tasa de flash — AccessKit no provee límite de fotosensibilidad ni "reduced motion" a nivel de motor, así que F3 permanece como implementación custom correcta. No conflacionar ambos. + +**Nota bidireccional**: la relación con Reliquias/Bendiciones está **confirmada** — `reliquias-bendiciones.md` (Approved 2026-08-18) define el contrato en su sección UI Requirements (RU-1..RU-6), coincidiendo con lo que este GDD asumía. AC-U30/OQ-U4 desbloqueados. + +## Tuning Knobs + +*(Derivado directamente de las tablas de variables de F1–F3 — sin re-spawn de especialista; las fórmulas ya llevan rangos definidos.)* + +| Knob | Symbol | Type | Default | Safe Range | Qué se rompe en extremos | +|---|---|---|---|---|---| +| Umbral de disparo instantáneo | `T_instant` | float | 5.0s | 3.0–8.0s | <3.0s: una muerte en ~3–4s aún puede colarse con poco aviso; >8.0s: el readout salta abruptamente para amenazas que la histéresis habría surfaceado de forma suave, añadiendo ruido. Debe mantenerse `T_instant < T_show` siempre. | +| Umbral de aparición del readout | `T_show` | float | 30.0s | 20.0–40.0s | <20s: la ventana de visibilidad cae bajo el objetivo de ~30s de Héroes (OQ-6); >40s: el readout aparece demasiado pronto y pierde urgencia, compitiendo por atención con el resto del HUD | +| Umbral de desaparición del readout | `T_hide` | float | 38.0s | `T_show`+4 a `T_show`+15 | Por debajo del piso: la banda muerta se estrecha demasiado y vuelve el parpadeo; por encima del techo: el readout queda "pegado" mucho después de que el peligro pasó | +| Duración mínima visible | `min_visible_s` | float | 4.0s | 2.0–8.0s | <2.0s: aún vulnerable a parpadeo bajo oscilación rápida; >8.0s: respuesta lenta a una recuperación real del héroe | +| Ventana de debounce de entrada | `debounce_on_s` | float | 0.5s | 0.2–1.5s | <0.2s: picos de un solo frame disparan falsos positivos; >1.5s: el peligro real tarda en anunciarse, violando "informar, nunca emboscar" (Pilar 4) | +| Amplitud de pulso de viñeta | `amplitude` | float | 0.25 | 0.15–0.35 | <0.15: el pulso es casi invisible sobre la base, anulando su propósito de canal redundante; >0.35: combinado con la base del tercio 3 (0.75) satura cerca de 1.0, perdiendo granularidad de escalada | +| Duración del pulso de feedback de Sacrificio | `sacrifice_feedback_pulse_s` | float | 0.15s | 0.08–0.3s | <0.08s: imperceptible en pantallas de alta tasa de refresco; >0.3s: se siente lento, retrasa la siguiente orden del jugador. *(Refina Core Rule 7: "un frame" era dependiente de framerate — este knob lo vuelve una duración real.)* | + +**Dependencia de tuning conjunto (flageada en review)**: el default de `T_hide`=38.0s solo cae dentro de su propio rango seguro (`T_show`+4 a `T_show`+15) cuando `T_show`=30.0. Si `T_show` se re-tunea a un extremo sin re-tunear `T_hide`, el default queda fuera de banda — p. ej. a `T_show`=20.0 el techo seguro es 35.0s, por debajo del default 38.0s. **Regla**: al mover `T_show`, recomputar y re-validar `T_hide` (y confirmar `T_instant < T_show`). Estos tres no son independientes. + +**No son knobs propios (referenciados, no duplicados)**: `base(third)` {0.25, 0.50, 0.75} y `freq(third)` {1,2,3 Hz} están bloqueados por la identidad de "3 tercios" y el tope duro de fotosensibilidad (KV-10) — no se exponen como configurables. `telegraph_duration_s` (dueño: Encuentro con Kaiju) y `guard_radius` (dueño: Sistema de Héroes) son consumidos por F3 y el anillo de marca respectivamente, pero cambiarlos se hace en sus GDDs dueños — re-escalan este sistema automáticamente sin tocar UI/HUD. + +## Visual/Audio Requirements + +*(`art-director` consultado — REQUIRED, no opcional, para sistemas de categoría UI. Leyó `design/art/art-bible.md` completo.)* + +**Health bars** (CU-1, CV-1): fill drena Hueso→Sangre Vieja (art bible 4.4), animación lineal sin rebote elástico (7.3 — el rebote lee como arcade, rompe el tono reverente). Marco en Tier 1 de 7.4 (rectángulo simple con reborde emplomado, Hueso) — nunca dorado, no es de origen relicario. Umbrales (50%/25%/crítico) disparan una señal diegética adicional (grietas extendiéndose en el marco, o un motivo de brasa/vela apagándose) + escalera de audio + pulso háptico en crítico — el tercer canal redundante más allá de la longitud de la barra. + +**Contorno de selección** (3.3/4.4): Acero Violeta-Ceniza, solo contorno sin relleno, con pulso de "respiración" + carillón suave (7.5). Prioridad hero>tropa se lee por una muesca de forma en el contorno del héroe, posicionada en su elemento asimétrico — no por tamaño. Tropa = anillo cerrado simple; héroe = mismo anillo con una interrupción de forma. + +**Timer ritual**: sin precedente directo en el art bible — vasija/arco en Ocre/Piedra Fría, con el tinte del relleno escalando hacia Acero Violeta-Ceniza a medida que crece el dread (NO hacia Hueso/Sangre Vieja, que 4.2 reserva estrictamente para el campo semántico vida/costo). `PREPARATION`: drenaje lento, mínimo movimiento. `IMMINENT`: reutiliza el motivo de grieta de vitral de 2.2/2.5 — fracturas capilares avanzan sobre la vasija, la tasa de drenaje se acelera visualmente. `CLIMAX`: las fracturas se resuelven / la vasija se rompe o se enciende hacia el estado de combate. El tinte de color permanece terciario, según el requisito de escalada multi-canal. *Open Question: la silueta exacta del contenedor no está especificada en ningún lado del art bible — necesita un pase de concepto, no solo una regla escrita.* + +**Readout de veredicto del héroe** (U-1..U-4): anclado al elemento asimétrico (5.1/5.2), mapeado sobre la lógica de tiers de marco ya existente en 7.4 en vez de inventar una gramática nueva. Delgada = tratamiento tipo Tier-1 (línea simple delgada) sobre el elemento. Sólida = tratamiento tipo Tier-2 (sólido/más grueso, con detalle de esquina), aún en Hueso/Piedra Fría — sin dorado. **Legendaria**: acento dorado restringido — una línea emplomada dorada delgada trabajada en la construcción de la silueta, no el bloom/glow completo que 4.5 reserva como sello exclusivo del dorado post-muerte. *(Decisión del usuario: SÍ introducir este acento dorado pre-muerte, distinto del tratamiento de reliquia confirmada — riesgo aceptado conscientemente.)* + +**Viñeta periférica de telegrafío del kaiju** (KV-1b): eco de pantalla del motivo de grieta 2.2, ya poseído por el telegrafío del cuerpo del kaiju (encuentro-con-kaiju.md). Tercio 1 (0–4.6s): pulso tenue, lento, de borde suave. Tercio 2 (4.6–9.3s): frecuencia de pulso sube, fracturas capilares avanzan desde el borde. Tercio 3 (9.3–14s): pulso más rápido (tope duro ≤3/seg, KV-10), fracturas totalmente formadas, viñeta de mayor contraste; el color vira hacia un eco cálido de borde (espejando la inversión "luz a través del kaiju" de 2.2) como cue estrictamente terciario. Modo flash-reducido sustituye por una rampa suave de brillo/densidad a través de los mismos tercios, sin parpadeo discreto. *Open Question: la escalada de audio no está especificada en el art bible — coordinar cadencia con sound-designer.* + +**Anillo de `guard_radius`**: anillo delgado sin relleno (convención de 4.4), pero en la familia de color de esbirros/kaiju (`#5A6B72`, 4.2/4.4) en vez del Acero de selección del jugador — mantiene sin ambigüedad de quién es el marcador (concepto de targeting propiedad del kaiju, no de selección del jugador). Sin pulso por defecto; el lenguaje de pulso queda reservado para selección (7.5) salvo que encuentro-con-kaiju.md especifique lo contrario. + +**Feedback de orden de Sacrificio** (contrato provisional, Core Rule 7): vocabulario nuevo, sin precedente en el art bible — adoptado conscientemente en esta sesión. Cursor pendiente: silueta solemne de reliquiario, Piedra Fría/Hueso neutro, sin dorado (la muerte aún no ocurrió, el test de la Principio A no se cumple). Pulso de confirmación: bloom lento único sobre la silueta del héroe, eco del motivo de luz cálida única de 2.3 (anticipatorio, no dorado pleno). Pulso de rechazo: flash frío de Acero con firma de movimiento distinta (doble parpadeo rápido vs. el bloom lento único de aceptación) — el color nunca es el único diferenciador. *(Decisión del usuario: adoptado como propuesto; queda como candidato a incorporarse formalmente al art bible cuando se audite ese documento.)* + +**Regla dura en todos los elementos anteriores**: sin texto editorializante en ningún caso, sin números de daño flotantes, ≥3 canales redundantes por cada señal de escalada (forma/marco + movimiento como primarios, color como terciario). + +📌 **UX Flag**: `/ux-design` debe correr contra este HUD antes de escribir epics — la silueta de la vasija del timer, el vocabulario del cursor de Sacrificio, y el detalle de targeting con gamepad/sin cursor necesitan un pase de flujo de interacción más allá de la dirección de arte. + +## UI Requirements + +Como UI/HUD *es* el sistema de UI, esta sección consolida los requisitos numerados y testeables establecidos en Detailed Design y Visual/Audio en un checklist limpio que Acceptance Criteria mapea directamente: + +- **HU-1**: Barras de vida visibles solo si la unidad está seleccionada O por debajo de vida máxima (Core Rule 3). +- **HU-2**: El contorno de selección diferencia héroe de tropa por una muesca de forma, nunca solo por tamaño o color (Core Rule 4, hereda U-6/AC-H33 de Héroes). +- **HU-3**: El timer ritual siempre muestra la fase actual + tiempo restante mientras la era está activa, en forma diegética, con las 3 fases distinguibles a simple vista (Core Rule 5). +- **HU-4**: El readout de veredicto del héroe es visible solo cuando `hero_risk_state=ON` (F1, por cualquiera de sus dos caminos — histéresis o disparo instantáneo `0; 00. Cuando el kaiju no engancha al héroe, kaiju_projected_dps=0 y time_to_death se expone como 'no aplica' (AC-K78), no un centinela 0/∞." + added: 2026-08-12 + revised: "" + + - name: hero_risk_threshold + status: active + source: design/gdd/ui-hud.md + referenced_by: + - design/gdd/ui-hud.md + variables: + - time_to_death # de Encuentro con Kaiju (F2), kaiju-only + - incoming_damage_rate_total # de Encuentro con Kaiju (F6), todas las fuentes + - hero_current_health # stat en vivo del héroe + - danger_time_s # = min(time_to_death, hero_current_health / max(incoming_damage_rate_total, ε)) + - T_show # const UI/HUD, MVP 30.0s (rango 20.0–40.0) + - T_hide # const UI/HUD, MVP 38.0s (T_show+4 a T_show+15) + - min_visible_s # const UI/HUD, MVP 4.0s (rango 2.0–8.0) + - debounce_on_s # const UI/HUD, MVP 0.5s (rango 0.2–1.5) + output_range: [0, 1] # booleano hero_risk_state {OFF=0, ON=1} + expression: "hero_risk_state = hysteresis(danger_time_s, T_show, T_hide, min_visible_s, debounce_on_s); danger_time_s = min(time_to_death, hero_current_health / max(incoming_damage_rate_total, ε))" + notes: "DUEÑO: UI/HUD (F1). Resuelve Héroes OQ-6 (valor concreto de la ventana de ~30s) y Encuentro con Kaiju OQ-14 (modelo de histéresis/anti-flicker). Controla la visibilidad del readout de veredicto del héroe (U-3). min() elige la fuente MÁS peligrosa (kaiju proyectado vs. enjambre de esbirros), nunca promedia — así una muerte rápida por esbirros no queda enmascarada por un timer de kaiju lento. El HP se divide por el DPS para volver incoming_damage_rate_total (una tasa) comensurable con time_to_death (un tiempo); ε=0.001 evita división por cero cuando no hay fuentes enganchadas. Histéresis: ON solo si danger_time_s≤T_show sostenido ≥debounce_on_s; OFF solo si danger_time_s≥T_hide Y llevaba visible ≥min_visible_s. Los 4 knobs son internos de UI/HUD (no registrados como constantes) y afinables en vertical slice (OQ-U6). Consume time_to_death e incoming_damage_rate_total — ambos owned por Encuentro con Kaiju." + added: 2026-08-12 + revised: "" + + - name: world_to_screen_transform + status: active + source: design/gdd/ui-hud.md + referenced_by: + - design/gdd/ui-hud.md + variables: + - world_position_world # posición a proyectar (héroe/marcador) + - camera_position_world # foco actual de la cámara + - viewport_size_px # dimensiones del viewport + - zoom # [0.5, 1.5], zoom_min/zoom_max + - world_unit_scale # 48 px/world unit a zoom 1.0 + output_range: [null, null] # px, SIN acotar — puede caer fuera del viewport (héroe fuera de pantalla) + expression: "viewport_size_px / 2 + (world_position_world - camera_position_world) * (world_unit_scale / zoom)" + notes: "DUEÑO: UI/HUD (F2). Inversa formal de screen_to_world_transform (owned por control-y-seleccion-de-unidades.md, cuya nota ya anticipaba que UI/HUD, minimapa y VFX necesitarían px_per_world_unit = world_unit_scale/zoom). Todo elemento anclado a mundo lo usa: barras de vida, ancla del readout de peligro, anillo de guard_radius. Salida SIN clamp — un resultado válido puede caer fuera de [0, viewport_size_px]; elementos que deben permanecer visibles fuera de pantalla (indicadores de borde) aplican su propio clamp aparte. Cross-check: cámara (100,50), zoom 1.0, viewport 1920×1080, objetivo (120,50) → (1920,540). Futuros consumidores esperados: minimapa y VFX (aún sin GDD)." + added: 2026-08-12 + revised: "" + + - name: blessing_magnitude + status: active + source: design/gdd/reliquias-bendiciones.md + referenced_by: + - design/gdd/reliquias-bendiciones.md + # Consumidor provisional: combate-dano.md aplica el bono plano resultante vía apply_damage / effective_attack_damage (OQ-RB1, sin read-hook aún) + variables: + - base_value # magnitud Delgada del efecto por categoría (offensive 8, defensive 30, rally 2) + - solid_factor # MVP 1.5 (rango 1.3–1.75) + - legendary_factor # MVP 2.25 (rango 1.8–2.75) + - tier # Delgada/Sólida/Legendaria (de Forja FL1) → tier_factor 1.0/solid_factor/legendary_factor + output_range: [1, null] # ≥ base_value ≥ 1; sin cota superior de fórmula + expression: "round_half_up(base_value * tier_factor(tier)) con tier_factor(Delgada)=1.0, tier_factor(Sólida)=solid_factor, tier_factor(Legendaria)=legendary_factor" + notes: "DUEÑO: Reliquias/Bendiciones (F-RB2). Escala SOLO la magnitud del efecto por tier; el tier nunca cambia QUÉ hace la bendición (Regla 3). Consume el tier ya resuelto por Forja de Legado (FL1) — NUNCA recalcula relic_quality ni redefine los cortes de tier (esos los posee Héroes vía relic_quality). Invariante loud-fail: magnitude estrictamente creciente entre los 3 tiers para cada base_value (atrapa colapso por redondeo, p.ej. base=1/solid=1.3→round(1.3)=1=Delgada). Bonos MVP: offensive +8/+12/+18 (daño plano temporal), defensive +30/+45/+68 (vida máx temporal), rally 2/3/5 (refuerzos, redondea hacia arriba). Todo bono es aditivo plano — NUNCA multiplicador porcentual (respeta Combate Regla 5)." + added: 2026-08-16 + revised: "" + + - name: effective_attack_damage + status: active + source: design/gdd/reliquias-bendiciones.md + referenced_by: + - design/gdd/reliquias-bendiciones.md + - design/gdd/combate-dano.md # PROVISIONAL — Combate deberá exponer el read-hook (no está en combate-dano.md hoy, OQ-RB1); aditivo a su contrato, no contradicción + variables: + - u # instancia de unidad (héroe/tropa), NO el tipo + - s # stat: {attack_damage, max_health} (MVP) + - base_stat # de CombatProfile/HeroDefinition del TIPO (solo lectura, nunca mutado) + - active_modifiers # modificadores de instancia efímeros activos sobre u para s (por invocation_id único) + - magnitude # bono plano por modificador (de blessing_magnitude) + output_range: [1, null] # ≥ base_stat ≥ 1; aditivo sin cap en MVP (limitador = economía de cargas) + expression: "effective_stat(u,s,t) = base_stat(u_type,s) + Σ magnitude(m) para cada m en active_modifiers(u,s,t)" + notes: "CONTRATO PROVISIONAL (OQ-RB1) — Reliquias/Bendiciones (F-RB1) lo define; Combate/Daño deberá exponer un read-hook effective_attack_damage(instancia) que capa los modificadores temporales sobre el CombatProfile del TIPO al resolver el golpe. combate-dano.md (Approved) NO lo tiene hoy: su CombatProfile es por-tipo, sin capa de instancia. Añadido pequeño/aditivo a su contrato, mismo patrón que Permadeath↔Kaiju con game_time_paused. Reliquias/Bendiciones NUNCA escribe en CombatProfile (Regla 8 / AC-RB26). Modificadores keyeados por (relic_id, unit_instance): re-elegir una reliquia YA activa sobre la misma unidad REFRESCA su duración, no apila una 2ª instancia (AC-RB31, revisión 2026-08-18 — acota el burst degenerado); reliquias DISTINTAS del mismo stat sí apilan aditivamente, acotadas por la cota worst-case computable de F-RB1 (AC-RB31b). A confirmar y capturar en el ADR de patrón de modificadores antes de implementar. Bloquea AC-RB32." + added: 2026-08-16 + revised: "" # Example: # @@ -139,7 +419,158 @@ formulas: [] # # Required fields: name, status, source, referenced_by, value, unit, added -constants: [] +constants: + - name: world_unit_scale + status: active + source: design/gdd/datos-de-era-civilizacion.md + referenced_by: + - design/gdd/datos-de-era-civilizacion.md + - design/gdd/input.md + - design/gdd/control-y-seleccion-de-unidades.md + - design/gdd/ui-hud.md # world_to_screen_transform (F2) lo usa para proyectar overlays anclados a mundo + value: 48 + unit: px_per_world_unit + added: 2026-07-23 + revised: "" + + - name: camera_pan_speed + status: active + source: design/gdd/datos-de-era-civilizacion.md + referenced_by: + - design/gdd/datos-de-era-civilizacion.md + - design/gdd/input.md + - design/gdd/control-y-seleccion-de-unidades.md + value: 30 + unit: world_units_per_sec + added: 2026-07-23 + revised: "" + # Nota: control-y-seleccion-de-unidades.md aplica compensación por zoom + # (effective_pan_speed = camera_pan_speed × zoom, Fórmula 4). El valor base 30 (a zoom 1.0) no cambia. + + - name: zoom_min + status: active + source: design/gdd/datos-de-era-civilizacion.md + referenced_by: + - design/gdd/datos-de-era-civilizacion.md + - design/gdd/input.md + - design/gdd/control-y-seleccion-de-unidades.md + - design/gdd/ui-hud.md # world_to_screen_transform (F2) opera con zoom en [zoom_min, zoom_max] + value: 0.5 + unit: zoom_multiplier + added: 2026-07-23 + revised: "" + + - name: zoom_max + status: active + source: design/gdd/datos-de-era-civilizacion.md + referenced_by: + - design/gdd/datos-de-era-civilizacion.md + - design/gdd/input.md + - design/gdd/control-y-seleccion-de-unidades.md + - design/gdd/ui-hud.md # world_to_screen_transform (F2) opera con zoom en [zoom_min, zoom_max] + value: 1.5 + unit: zoom_multiplier + added: 2026-07-23 + revised: "" + + - name: reinforcement_ratio + status: active + source: design/gdd/sistema-de-tropas.md + referenced_by: + - design/gdd/sistema-de-tropas.md + value: 0.25 + unit: fraction + added: 2026-07-23 + revised: "" + # Rango tuneable 0.15–0.35. La UI de Preparación (sin GDD) consumirá max_reinforcements derivado de este ratio. + + - name: telegraph_duration_s + status: active + source: design/gdd/encuentro-con-kaiju.md + referenced_by: + - design/gdd/encuentro-con-kaiju.md + - design/gdd/ui-hud.md # telegraph_vignette_intensity (F3) reparte los 3 tercios de la viñeta periférica sobre esta duración + value: 14.0 + unit: seconds + added: 2026-08-10 + revised: 2026-08-12 # ui-hud.md añadido a referenced_by (F3 consume la duración para el canal periférico KV-1b) + # Rango seguro [12.5,17.0]. Mínimo matemático duro: 12.0s exacto (por debajo rompe la garantía de ~30s + # de Héroes AC-H29 para el héroe de menor TTK crudo — no es solo un valor de feel, ver Encuentro con + # Kaiju F2/AC-K21/AC-K22). Futuros consumidores esperados: Audio (KA-1) y UI/HUD (KU-1), sin GDD aún. + + - name: guard_radius + status: active + source: design/gdd/sistema-de-heroes.md + referenced_by: + - design/gdd/sistema-de-heroes.md + - design/gdd/encuentro-con-kaiju.md # KV-2 renderiza el radio; una muerte dentro de él satisface P=1 vía defensiva (Regla 5) + - design/gdd/combate-dano.md # AC-C32 computa P=1 vía defensiva: muerte dentro de guard_radius de un marked_target + - design/gdd/ui-hud.md # renderiza el anillo secundario de guard_radius (Core Rule 9 / HU-7); la runa de suelo la posee Encuentro (KV-2) + value: 4.0 + unit: world_units + added: 2026-08-12 + revised: "" + # Rango seguro 2.5–6.0 (Héroes). Radio alrededor de un objetivo/aliado marcado dentro del cual una + # muerte lo protege (vía defensiva de P). <2.5: proteger casi imposible; >6.0: estar vagamente cerca + # ya cuenta. Valor final confirmado con Combate/Kaiju (Héroes OQ-2). Encuentro con Kaiju lo cita como + # =4.0 (KV-2); Combate lo usa en la clasificación P (AC-C32). + + - name: invocation_charges + status: active + source: design/gdd/reliquias-bendiciones.md + referenced_by: + - design/gdd/reliquias-bendiciones.md + # Posibles referenciadores (a confirmar): datos-de-era-civilizacion.md (EraDefinition puede autorar el valor por era, OQ-RB4); ui-hud.md (renderiza cargas restantes, RU-1) + value: 3 + unit: charges_per_era + added: 2026-08-16 + revised: "" + # Rango seguro 2–4. Recurso finito por era (anti-farmeo, Regla 5): invocar gasta 1; se resetea por era, NO se acumula. + # <2: una sola decisión en un CLIMAX largo. >4: erosiona escasez + carga cognitiva en tiempo real (sin pausa). + + - name: draw_size + status: active + source: design/gdd/reliquias-bendiciones.md + referenced_by: + - design/gdd/reliquias-bendiciones.md + # Posibles referenciadores (a confirmar): datos-de-era-civilizacion.md (posible override por era); ui-hud.md (renderiza la oferta 3-choose-1, RU-3) + value: 3 + unit: relics_per_draw + added: 2026-08-16 + revised: "" + # Rango seguro 2–4 (≥1 duro). Sorteo Hades-style: gastar carga → ofrecer draw_size reliquias → elegir 1 (F-RB3). + # Si P4: demasiadas opciones bajo presión sin pausa. + + - name: pool_cap + status: active + source: design/gdd/reliquias-bendiciones.md + referenced_by: + - design/gdd/reliquias-bendiciones.md + # Posibles referenciadores futuros: salon-conmemorativo-panteon.md (#14, sin GDD — exhibe las reliquias retiradas del pool activo por FIFO); datos-de-era-civilizacion.md (posible override) + value: 16 + unit: relics + added: 2026-08-16 + revised: "" + # Rango seguro 10–24. Tope del pool invocable (F-RB5, Rule A): al exceder, la reliquia de menor forge_index se retira a + # solo-exhibición en Panteón (la RelicRecord NUNCA se borra). Acota el crecimiento del pool entre eras (riesgo ALTO flageado). + # <10: variedad de sorteo colapsa. >24: reliquias dejan de ser reconocibles/especiales. + + - name: max_band_size + status: active + source: design/gdd/datos-de-era-civilizacion.md # autorado por era en EraDefinition (dueño); NO cuenta héroes + referenced_by: + - design/gdd/datos-de-era-civilizacion.md + - design/gdd/sistema-de-tropas.md # Fórmula E: enforcement + expone band_headroom = max(0, max_band_size − alive_troop_count); AC-32/33/34 + - design/gdd/reliquias-bendiciones.md # rally surte min(rally_N, band_headroom), read-only (OQ-RB7, F2-B5) + value: per-era # autorado por EraDefinition; sin valor global único. Recom. ~1.5–2× el roster inicial de tropas + unit: troops + added: 2026-08-21 + revised: "" + # Cierra F2-B5 (tope de banda para rally). Tope de TROPAS simultáneas vivas del jugador esta era; ambos grifos + # (reclutamiento de PREPARATION Fórmula C + rally de CLIMAX de Reliquias) respetan este techo total. Restricción de + # autoría (loud-fail al cargar EraDefinition, AC-34): max_band_size ≥ Σ starting_count_[tipo] del unit_roster. + # band_headroom es su salida derivada read-only (Fórmula E), no un knob independiente — no se registra por separado. + # = roster inicial: rally y reclutamiento siempre no-op. Muy alto: diluye escasez del Pilar 3 + presupuesto de entidades. # Example: # diff --git a/docs/architecture/adr-0001-combat-stat-modifier-layer.md b/docs/architecture/adr-0001-combat-stat-modifier-layer.md new file mode 100644 index 0000000000..31ad2e188e --- /dev/null +++ b/docs/architecture/adr-0001-combat-stat-modifier-layer.md @@ -0,0 +1,198 @@ +# ADR-0001: Capa de modificadores de stat por instancia (dueño: Combate/Daño) + +## Status +Accepted (2026-08-21) + +## Date +2026-08-18 + +## Engine Compatibility + +| Field | Value | +|-------|-------| +| **Engine** | Godot 4.6 | +| **Domain** | Core / Scripting (modelo de datos de combate) | +| **Knowledge Risk** | LOW | +| **References Consulted** | `docs/engine-reference/godot/VERSION.md` (no existe módulo Core/Scripting en la referencia; el dominio no toca Physics/Rendering) | +| **Post-Cutoff APIs Used** | Ninguna. Composición por nodos, `Array[T]` tipado, `RefCounted`, y `_physics_process` de paso fijo son estables desde Godot 4.0 — no dependen de nada introducido en 4.4/4.5/4.6 | +| **Verification Required** | (1) Confirmar que el contador de tick que alimenta `expiry_tick` es **la misma fuente** en Combate y en Reliquias (F-RB4). **SUPERSEDED por ADR-0002 (Accepted 2026-08-21): la fuente es `TimeControl.game_tick` (pause-aware / scale-aware), NO `Engine.get_physics_frames()`** (que no se detiene en pausa — ver Risks). `sweep_expired(tick)` se ancla a `TimeControl.game_tick`; una divergencia de fuente rompería la expiración. (2) Confirmar en 4.6 que un `Array[StatModifier]` tipado sobre una subclase de `RefCounted` se limpia sin fugas al liberar la instancia de unidad. (3) **Assert al arranque `Engine.physics_ticks_per_second == 60`** — `F-RB4` computa `duration_ticks` sobre un supuesto fijo de 60 tps; si un project setting cambia el tps, `duration_ticks` se desincroniza de la duración en segundos sin error. (4) Verificar en inspector/`.tres` que `CombatProfile.resource_local_to_scene` sea `false` (default) — si estuviera `true`, Godot duplicaría el Resource por instancia de escena silenciosamente (rompe "compartido por tipo" sin aviso). | + +## ADR Dependencies + +| Field | Value | +|-------|-------| +| **Depends On** | None | +| **Enables** | ADR futuro de `offering_time_scale` / pausa (F2-B4/S2) puede asumir que la capa de instancia ya existe; ADR futuro de persistencia de Guardado (define cómo/si se serializan los modificadores activos — F2-B7/S4) | +| **Blocks** | Epic de Combate/Daño y epic de Reliquias/Bendiciones no pueden empezar implementación hasta que este ADR sea Accepted (definen la superficie `CombatState` + API de modificadores) | +| **Ordering Note** | Primer ADR del proyecto. Debe ser Accepted antes de `/create-epics` sobre Combate o Reliquias. La persistencia del estado efímero de invocación queda explícitamente fuera de alcance y se difiere al ADR de Guardado. | + +## Context + +### Problem Statement +La cadena del Pilar 2 (muerte → reliquia → bendición → poder en combate) no tiene dónde vivir en el modelo de datos actual. `reliquias-bendiciones.md` (F-RB1) requiere un **stat efectivo por instancia** — `effective_stat(u, s, t) = base_stat(u_type, s) + Σ magnitude(m)` — que Combate debe leer **en el momento de resolver un golpe**, vía un read-hook que Reliquias llama `effective_attack_damage(instancia)`. Pero `combate-dano.md` modela el `CombatProfile` estrictamente **por tipo de unidad** (Regla 2/F1); `apply_damage` lee un `attack_damage` estático y no existe ninguna capa de instancia. El símbolo `effective_attack_damage` aparece 11× en Reliquias y 0× en Combate; el registro lo marca `PROVISIONAL`. La revisión cross-GDD 2026-08-18 lo elevó a bloqueante de arquitectura (F2-B1, detectado independientemente por los dos agentes de consistencia y confirmado por el walkthrough de Fase 4). Reliquias declara además que **nunca escribe en `CombatProfile`** — necesita un canal explícito para depositar modificadores sin mutar el Resource compartido por tipo. + +### Constraints +- **Combate/Daño Regla 1** — "un solo camino de daño"; todo el daño pasa por una única operación `apply_damage`. El valor efectivo debe computarse **dentro** de Combate, no en un cálculo paralelo aguas arriba. +- **Combate/Daño Regla 5** — daño plano, sin multiplicadores de tipo (única excepción nombrada hoy: la Furia del Kaiju, ×1.5). Los modificadores de bendición son **planos aditivos** (`+N` enteros), así que respetan la regla sin ampliarla. +- **`CombatProfile` es contenido por tipo** — un Resource compartido entre todas las instancias de un tipo. Nunca debe mutarse en runtime (mutarlo afectaría a todas las instancias de ese tipo y corrompería el dato-fuente). +- **DI sobre singletons** (coding-standards del proyecto) — los sistemas deben ser testeables en aislamiento; nada de acoplamiento a Autoloads por nombre. +- **Loud-fail sobre clamp silencioso** (precedente de Héroes/Combate) — config inválida falla ruidosamente al cargar. +- **Determinismo por tick de física** — F-RB4 ancla la expiración al paso fijo de física, nunca a wall-clock/`_process(delta)`. +- **Capas** — Reliquias (Legado, Feature) ya depende de Combate (Gameplay, Feature, Approved). La solución no debe invertir esa dirección ni crear un ciclo. + +### Requirements +- Combate debe exponer un stat **efectivo por instancia** legible en resolución de golpe, sobre el `CombatProfile` por tipo, sin mutar el Resource. +- Reliquias debe poder **añadir/retirar** modificadores planos aditivos por instancia, por stat, con un `expiry_tick`, sin tocar `CombatProfile`. +- El modelo debe soportar los dos stats del MVP: `attack_damage` y `max_health`. +- La dedup "una instancia activa por `relic_id` por unidad, refresh-on-repick" (F-RB1) debe ser expresable. +- La expiración debe verificarse **al inicio de cada tick, antes de la fase-1 de daño de Combate** (F-RB4), sin ambigüedad de orden. +- Testeable sin escena completa; sin RNG ni I/O en el cálculo del stat efectivo. + +## Decision + +**Combate/Daño posee una capa de stat de combate por instancia que se apoya sobre el `CombatProfile` por tipo.** Se introduce un componente `CombatState` por instancia de unidad que contiene una referencia **solo-lectura** al `CombatProfile` compartido del tipo más una lista de modificadores activos. Combate expone: + +- una **API de escritura** que las fuentes de modificadores (Reliquias en el MVP) llaman para depositar/retirar bonos planos aditivos, y +- un **read-hook** `effective_attack_damage(instance)` (y en general `effective_stat`) que Combate lee **internamente** al resolver un golpe en `apply_damage`. + +Reliquias posee la **política** (qué modificador, qué magnitud, qué duración, cuándo) pero **no** la aplicación en resolución de golpe. Combate posee la **aplicación** y la **expiración**. Reliquias nunca lee ni escribe `CombatProfile`; solo llama la API de modificadores. Esto mantiene toda la matemática de daño dentro de Combate (Regla 1), mantiene los bonos planos aditivos (Regla 5, sin ampliarla), y conserva la dirección de dependencia Reliquias→Combate (sin ciclo). + +**Cómputo pull + barrido de expiración en Combate.** `effective_attack_damage` suma los modificadores activos **bajo demanda** en `apply_damage` (el stack es ≤3; coste despreciable). Cada modificador lleva su `expiry_tick`; la capa de Combate trata `t ≥ expiry_tick` como inactivo y **barre** los expirados al inicio del tick, antes de la fase-1 de daño. Reliquias nunca sondea: llama `add_modifier(…, expiry_tick)` y se olvida. Esto satisface el orden de F-RB4 dentro del sistema dueño. + +**`StatModifier` es un objeto de runtime liviano** (subclase de `RefCounted`), no un Resource: `{ stat, magnitude, expiry_tick, source_relic_id }`. Sin overhead de serialización a disco; `source_relic_id` habilita la dedup refresh-on-repick (F-RB1) y deja que el futuro ADR de persistencia decida por separado si/cómo se guardan los modificadores activos. + +**Alcance MVP: dos stats** — `attack_damage` y `max_health` (los que nombra F-RB1). La estructura es extensible a más stats sin cambio de forma, pero este ADR solo se compromete a esos dos. + +### Architecture Diagram + +``` + CombatProfile (Resource, POR TIPO — contenido, inmutable en runtime) + │ attack_damage, max_health, attack_range, aggro_range, leash_range, attack_cooldown_s + │ (referencia solo-lectura, compartida por todas las instancias del tipo) + ▼ + ┌───────────────────────────────────────────────┐ + │ CombatState (POR INSTANCIA — dueño: Combate) │ + │ - base_profile: CombatProfile (read-only) │ + │ - _modifiers: Array[StatModifier] │ + │ │ + │ + effective(stat) -> int (pull, on-read) │ + │ + effective_attack_damage() -> int │ + │ + add_modifier(mod) / remove_by_source(id) │ + │ + sweep_expired(tick) (inicio de tick) │ + └───────────────────────────────────────────────┘ + ▲ escribe modificadores ▲ lee effective_* en apply_damage + │ (add/remove) │ (interno a Combate) + │ │ + Reliquias/Bendiciones Combate/Daño (apply_damage, Regla 1) + (política: qué/cuándo/cuánto) (aplicación + expiración) + + Furia del Kaiju: NO pasa por esta capa. Sigue su propio camino + apply_damage(target, amount, source) con `amount` computado por el + controlador del Kaiju (×1.5, excepción multiplicativa nombrada, Regla 5). +``` + +### Key Interfaces + +```gdscript +# StatModifier — objeto efímero de runtime (RefCounted), no Resource. +class_name StatModifier extends RefCounted +enum Stat { ATTACK_DAMAGE, MAX_HEALTH } # alcance MVP: 2 stats +var stat: Stat +var magnitude: int # bono plano aditivo, ≥1 (loud-fail si <1) +var expiry_tick: int # tick de física de expiración; −1 = instantáneo/no-temporizado +var source_relic_id: String # dedup refresh-on-repick (F-RB1) + trazabilidad + +# CombatState — componente por instancia (miembro del script de la unidad), dueño: Combate/Daño. +# RefCounted, NO Node: sweep_expired lo invoca el bucle de Combate (no es auto-conducido) y +# nada aquí usa árbol de escena / señales / _physics_process, así que no necesita ser Node. +# Se auto-libera por refcount al liberar la unidad (sin ordenar queue_free). Tradeoff aceptado: +# un RefCounted no aparece en el panel Remote Scene Tree del editor — el debug de buffs activos +# se hace vía un test/volcado dedicado, no por inspección en vivo del árbol. +class_name CombatState extends RefCounted +var base_profile: CombatProfile # referencia solo-lectura, compartida por tipo + +func effective(stat: StatModifier.Stat) -> int # pull: base + Σ modificadores activos +func effective_attack_damage() -> int # read-hook nombrado (grep CI) +func add_modifier(mod: StatModifier) -> void # refresh-on-repick por source_relic_id+stat +func remove_by_source(source_relic_id: String) -> void +func sweep_expired(current_tick: int) -> void # llamado al inicio de tick, antes de fase-1 + +# Dedup (F-RB1): add_modifier con un (source_relic_id, stat) ya presente NO apila — +# refresca expiry_tick de la instancia existente (la magnitud no cambia; mismo tier). +``` + +**Contrato de invariantes (loud-fail al añadir / al cargar):** +- `magnitude ≥ 1` para todo modificador plano. +- `stat ∈ {ATTACK_DAMAGE, MAX_HEALTH}` en el MVP. +- `expiry_tick > applied_tick` para modificadores temporizados (o `−1` para instantáneos). +- La capa **nunca** muta `base_profile` (el `CombatProfile` compartido). Un intento de escritura es un bug de programación, no un extremo válido. + +## Alternatives Considered + +### Alternative 1: Reliquias posee el stack; Combate lo consulta +- **Description**: Reliquias mantiene el registro de modificadores por instancia y expone una query; Combate llama a Reliquias (o a un servicio que Reliquias posee) al resolver el golpe. +- **Pros**: Reliquias tiene toda su lógica en un lugar; Combate no crece. +- **Cons**: Invierte la dirección de capas — Combate (core Approved) dependería de Reliquias (feature Legado). Crea exactamente el ciclo que `systems-index.md` afirma hoy que no existe. Rompe Combate Regla 1 (el valor efectivo se computaría fuera del único camino de daño). Deja a Combate inservible sin Reliquias presente, aunque el combate base no necesita bendiciones. +- **Rejection Reason**: Acoplamiento y dirección de dependencia inaceptables; viola Regla 1. + +### Alternative 2: Servicio neutral de modificadores en capa Core (poseído por nadie) +- **Description**: Un componente genérico de modificadores de stat por instancia en una capa inferior, del que dependen tanto Combate como Reliquias. +- **Pros**: Separación más limpia; reutilizable por Furia y futuros buffs; ambos sistemas como clientes simétricos. +- **Cons**: Más abstracción de la que el MVP necesita; introduce un sistema foundational nuevo que no está en el systems-index (cambio de alcance); Combate ya se declara el dueño del camino de daño, así que "poseído por nadie" duplica autoridad que Regla 1 ya asigna. +- **Rejection Reason**: Sobre-ingeniería para el MVP. Se puede refactorizar hacia esto más tarde si Furia u otras fuentes necesitan la misma capa; hoy Reliquias es la única fuente aditiva y Combate ya es el dueño natural. (Registrado como posible evolución en Consequences → Risks.) + +## Consequences + +### Positive +- La cadena de daño del Pilar 2 tiene un hogar concreto; `effective_attack_damage` deja de ser un símbolo fantasma. +- Toda la matemática de daño permanece dentro de Combate (Regla 1 intacta); los modificadores planos aditivos no amplían Regla 5. +- `CombatProfile` sigue siendo contenido inmutable por tipo; el estado por instancia vive aparte. +- Testeable en aislamiento: `CombatState` es un `RefCounted` que se instancia con `.new()` sin árbol de escena, con dependencias inyectables, sin Autoload, sin RNG, sin I/O. +- La expiración vive junto a la aplicación (un solo dueño del ciclo de vida del modificador en resolución), resolviendo el orden de F-RB4 sin polling cross-sistema. +- Da forma concreta al contrato que Reliquias marca provisional (cierra OQ-RB1 en cuanto se acepte y se refleje en ambos GDDs). + +### Negative +- Combate crece: gana un componente por instancia y una API de modificadores que antes no tenía. +- Cada instancia de unidad ahora carga un `CombatState` (`RefCounted` + lista). Coste de memoria pequeño pero real a escala de banda (decenas de unidades). +- Introduce un acoplamiento de escritura Reliquias→API-de-Combate que debe reflejarse en ambos GDDs (hoy Combate no lista la API; Reliquias la marca provisional). + +### Risks +- **Fuente de tick divergente**: si Combate y Reliquias leen contadores de tick distintos, `expiry_tick` se desincroniza. *Mitigación*: fijar una única fuente de tick de física en implementación (Verification Required) y grep de CI sobre el símbolo. +- **Mutación accidental de `CombatProfile`**: un programador podría escribir sobre `base_profile`. *Mitigación*: `base_profile` como referencia tratada solo-lectura + assert/`push_error` en debug si se detecta mutación; documentar en el control-manifest. +- **Evolución a Furia/otros buffs**: si Furia u otros efectos futuros necesitan modificadores, la Alternative 2 (servicio neutral) puede volverse preferible. *Mitigación*: `CombatState` como componente (no Autoload) hace ese refactor local; la API de modificadores ya es agnóstica de la fuente (`source_relic_id` es solo un id de trazabilidad). +- **Persistencia**: los modificadores activos son estado efímero; guardarlos/restaurarlos (recarga a mitad de era) NO se resuelve aquí. *Mitigación*: diferido explícitamente al ADR de Guardado (S4/F2-B7); `StatModifier` lleva los campos suficientes (`source_relic_id`, `expiry_tick`) para que ese ADR decida el rebasing de tick. +- **`resource_local_to_scene` (validación godot-specialist)**: si `CombatProfile.resource_local_to_scene` estuviera `true` (por accidente en el `.tres` o en una escena que herede), Godot **duplicaría** el Resource por instancia de escena en silencio — lo opuesto a "compartido por tipo", y sin error: los cambios de balance por tipo dejarían de propagarse. *Mitigación*: mantenerlo en `false` (default) y verificarlo (ver Verification Required 4). +- **`Engine.get_physics_frames()` no es pause-aware (validación godot-specialist)**: el contador global de física sigue avanzando bajo `SceneTree.paused` (la pausa detiene `_process`/`_physics_process`, no el contador del motor). Si la intención de diseño fuera "las bendiciones no expiran mientras el juego está pausado", anclar `expiry_tick` a `Engine.get_physics_frames()` **no** da esa conducta gratis. *Mitigación*: el **ADR futuro de `game_time_paused`/`offering_time_scale` debe decidir** entre (a) aceptar que los ticks avancen en pausa, o (b) introducir un contador de tick pause-aware, **antes** de fijar la semántica de `expiry_tick`. Registrado como input directo a ese ADR (ver Related Decisions). + +## GDD Requirements Addressed + +| GDD System | Requirement | How This ADR Addresses It | +|------------|-------------|--------------------------| +| `reliquias-bendiciones.md` | F-RB1 `effective_stat(u,s,t)` por instancia; read-hook `effective_attack_damage(instancia)`; "nunca escribe `CombatProfile`"; dedup refresh-on-repick por `relic_id` | Combate expone `CombatState.effective_attack_damage()` y una API `add_modifier/remove_by_source`; Reliquias solo llama esa API; dedup por `(source_relic_id, stat)` | +| `reliquias-bendiciones.md` | F-RB4 expiración por tick, chequeada antes de la fase-1 de daño; "expirar nunca mata" (clamp de `current_health`, no `apply_damage`) | `sweep_expired(tick)` al inicio de tick dentro de Combate; el clamp defensivo de `max_health`/`current_health` lo aplica el dueño del stat (Combate) sin pasar por `apply_damage` | +| `combate-dano.md` | Regla 1 (un solo camino de daño); Regla 5 (plano, sin multiplicadores); `CombatProfile` por tipo inmutable | El valor efectivo se computa dentro de `apply_damage`; modificadores planos aditivos; `CombatProfile` referenciado solo-lectura, nunca mutado | +| `encuentro-con-kaiju.md` | Furia ×1.5 como excepción multiplicativa nombrada (Regla 10) | La Furia **no** pasa por esta capa aditiva; sigue su camino `apply_damage(target, amount, source)` con `amount` del controlador del Kaiju. Registrado para cerrar la incoherencia F2-W3/W1 (Combate debe reconocer la excepción en su texto, cross-GDD aparte) | + +## Performance Implications +- **CPU**: Suma de ≤3 enteros por golpe resuelto (pull on-read) + un barrido de expiración O(nº modificadores) al inicio de tick. Despreciable frente al presupuesto de combate. +- **Memory**: Un `CombatState` (`RefCounted` + `Array[StatModifier]`) por instancia de unidad — sin registro en árbol de escena. A escala de banda (decenas de unidades) es pequeño; los modificadores son ≤3 por instancia y efímeros. +- **Load Time**: Nulo — sin assets nuevos; `CombatProfile` ya se carga por tipo. +- **Network**: N/A (sin multijugador en el MVP). + +## Migration Plan +No hay código aún (fase de diseño). En implementación: (1) `apply_damage` y el bucle de ataque leen `combat_state.effective_attack_damage()` en vez de `profile.attack_damage`; para unidades sin modificadores, `effective` devuelve el valor base (comportamiento idéntico al actual). (2) La forma del `CombatProfile` por tipo no cambia. (3) `combate-dano.md` gana la API de modificadores en su contrato; `reliquias-bendiciones.md` des-marca F-RB1/`effective_attack_damage` de "provisional" y cierra OQ-RB1 — edición cross-GDD a hacer al aceptar este ADR. + +## Validation Criteria +- Test unitario: `effective_attack_damage()` sin modificadores == `base_profile.attack_damage`. +- Test unitario: con dos modificadores ofensivos distintos activos, devuelve `base + m1 + m2`; con el mismo `source_relic_id` re-añadido, no apila (refresca `expiry_tick`). +- Test unitario: un modificador con `expiry_tick == t` no cuenta en el golpe del tick `t` (barrido antes de fase-1). +- Test unitario: expiración de bono defensivo de `max_health` con `current_health` alto → clampa hacia abajo, nunca lleva `current_health` a ≤0 (F-RB4 "expirar nunca mata"). +- Test de integración: Reliquias `add_modifier` → un golpe de Combate refleja el bono; tras `expiry_tick`, vuelve al base. +- CI: grep del símbolo `effective_attack_damage` presente en el código de Combate (contrato materializado, no fantasma). +- Loud-fail: `magnitude < 1`, `stat` fuera de dominio, o `expiry_tick ≤ applied_tick` en un temporizado → error ruidoso. + +## Related Decisions +- Futuro ADR: dueño y aplicación de `offering_time_scale` × `game_time_paused` × tick de física (F2-B4/S2) — asumirá esta capa de instancia **y debe decidir la semántica pause-aware de `expiry_tick`** (`Engine.get_physics_frames()` no se detiene en pausa — ver Risks). +- **ADR-0003** (persistencia de Guardado — `docs/architecture/adr-0003-save-persistence-serialization.md`, Proposed): resuelve la persistencia del estado efímero de invocación y de los modificadores activos. Se persiste `remaining_ticks` (relativo), **NO** el `expiry_tick` absoluto, rebasado al cargar contra `TimeControl.game_tick` (F2-B7/S4). Cierra el hand-off de persistencia que este ADR difirió. +- `design/gdd/gdd-cross-review-2026-08-18.md` — F2-B1 (origen de este ADR). +- `design/gdd/combate-dano.md`, `design/gdd/reliquias-bendiciones.md` — GDDs a reconciliar al aceptar. diff --git a/docs/architecture/adr-0002-time-control-service.md b/docs/architecture/adr-0002-time-control-service.md new file mode 100644 index 0000000000..68def0abb6 --- /dev/null +++ b/docs/architecture/adr-0002-time-control-service.md @@ -0,0 +1,220 @@ +# ADR-0002: Regímenes de tiempo de juego — servicio `TimeControl` (pausa, cámara lenta, game-tick) + +## Status +Accepted (2026-08-21) + +## Date +2026-08-18 + +## Engine Compatibility + +| Field | Value | +|-------|-------| +| **Engine** | Godot 4.6 | +| **Domain** | Core / Scripting (bucle de juego, orquestación de tiempo) | +| **Knowledge Risk** | LOW-MEDIUM | +| **References Consulted** | `docs/engine-reference/godot/VERSION.md`; `docs/engine-reference/godot/breaking-changes.md` (L38 — rearquitectura de interpolación de física **3D** en 4.5, movida de RenderingServer a SceneTree); `docs/engine-reference/godot/current-best-practices.md` (L35 — física **2D sin cambios** en 4.6) | +| **Post-Cutoff APIs Used** | Ninguna. La decisión **evita deliberadamente** `Engine.time_scale` y `SceneTree.paused` como mecanismo; usa un flag/escala cooperativos leídos por polling (patrón ya elegido por Permadeath Regla 4). `_physics_process` de paso fijo y contadores propios son estables desde 4.0. | +| **Verification Required** | (1) Confirmar en 4.6 que escalar el avance de lógica por `time_scale` (unidades avanzan `scale × delta_fijo` por frame de física, con física a 60 tps constante) produce movimiento suave bajo interpolación de física 2D — no cambiamos `physics_ticks_per_second`, así que la interpolación sigue teniendo un tick real por frame. *La rearquitectura L38 es solo de física **3D**; la 2D del MVP no se ve afectada (current-best-practices L35), así que esto es una verificación de suavidad, no un riesgo de la migración.* (2) Confirmar que ningún sistema de tiempo de juego lee `_process(delta)`/wall-clock en vez de `TimeControl` (grep de CI). (3) Assert al arranque `Engine.physics_ticks_per_second == 60` (compartido con ADR-0001). | + +## ADR Dependencies + +| Field | Value | +|-------|-------| +| **Depends On** | ADR-0001 (capa de modificadores de stat por instancia) — este ADR fija la fuente de tick pause-aware que ADR-0001 dejó abierta; ADR-0001 debe estar Accepted antes de que la implementación de expiración se ancle a `TimeControl.game_tick` | +| **Enables** | ADR futuro de persistencia de Guardado (F2-B7/S4) — el rebasing de `expiry_tick` en recarga se define contra `TimeControl.game_tick`, no contra un contador de motor | +| **Blocks** | Epics de Permadeath, Reliquias/Bendiciones, Temporizador de Preparación/Ritual, Combate/Daño y Encuentro con Kaiju no pueden empezar implementación de su lógica dependiente-de-tiempo hasta que este ADR sea Accepted (todos consumen `TimeControl`) | +| **Ordering Note** | Segundo ADR. Junto con ADR-0001 son los dos gates de arquitectura del review cross-GDD 2026-08-18 (F2-B1, F2-B4/S2). Resuelve además OQ-P4/F2-W4, OQ-RB9/S6 y la semántica pause-aware que ADR-0001 difirió. | + +## Context + +### Problem Statement +Tres regímenes de tiempo de juego conviven en el MVP y hoy **nadie los posee**: +- **Pausa total** — Permadeath Regla 4 expone `game_time_paused=true` durante el beat `DEATH_HOLD` (mecanismo **cooperativo**: un flag booleano sondeado por consumidores, **nunca** `SceneTree.paused`, para que el reloj propio del beat y la UI sigan corriendo). Consumidores confirmados: Combate (cooldowns, AC-C52), Kaiju (congela su ciclo de intención, Regla 11/AC-K89), Input (suprime entrada). +- **Cámara lenta de la oferta** — Reliquias Regla 7b/`offering_time_scale`=0.2 quiere ralentizar **uniformemente** la simulación completa (kaiju, telegrafío, unidades) mientras una invocación está abierta. Reliquias declara explícitamente (OQ-RB9) que **no posee** el reloj de simulación y que el mecanismo es "de nivel motor/loop principal". +- **Tiempo normal** — todo lo demás. + +De aquí salen cuatro bloqueantes/huecos del review cross-GDD 2026-08-18: +- **F2-B4 / S2**: nadie aplica `offering_time_scale`; Combate no lo lista como autoridad de tiempo (solo `game_time_paused`); dos lecturas divergentes (global vs por-sistema) sin adjudicar; y un loop de stalling de la oferta puede estancar el temporizador anti-turtle de Furia. +- **OQ-RB9 / S6**: la precedencia pausa>cámara-lenta está escrita solo en un Open Question de Reliquias, no ratificada; Permadeath no lista a Reliquias como consumidor de `game_time_paused`; no hay mecanismo de máquina de estados que "preserve" la oferta durante la pausa. +- **OQ-P4 / F2-W4**: el Temporizador (Approved) **nunca** lee `game_time_paused` — su reloj de era no está garantizado que se congele durante un beat. +- **Hand-off de ADR-0001**: `Engine.get_physics_frames()` no es pause-aware; la semántica pause-aware de `expiry_tick` se difirió explícitamente a este ADR. + +El patrón cooperativo (flag + polling, no `SceneTree.paused`/`Engine.time_scale`) ya fue elegido conscientemente por los GDDs para poder **eximir** sistemas selectivamente (el reloj del beat, la UI). La rearquitectura de interpolación de física 3D de 4.5 (breaking-changes L38) es una razón más para no escalar la física globalmente vía `Engine.time_scale`. + +### Constraints +- **Mecanismo cooperativo, no engine-native** — no usar `SceneTree.paused` (detiene TODOS los callbacks de proceso, imposibilita eximir el reloj del beat/UI sin gimnasia de `process_mode`) ni `Engine.time_scale` (escala la física global, incluidos sistemas que deben correr en tiempo real). +- **Permadeath Regla 4** — la pausa es una regla dura, un stop total; el reloj propio del beat corre en tiempo real (unscaled) para no deadlockear. +- **Determinismo por tick de física** — F-RB4 (Reliquias) cuenta expiración en ticks enteros; la física corre a 60 tps fijos. +- **DI sobre singletons** (coding-standards) — testeable en aislamiento; nada de acoplamiento a Autoloads por nombre para estado ajeno. +- **Precedencia ya pre-decidida por diseño** (OQ-RB9) — pausa > cámara lenta; la muerte es el beat emocional mayor (Pilar 1) y ya posee un stop total. +- **No romper contratos Approved** — Combate AC-C52, Kaiju Regla 11/AC-K89 ya leen `game_time_paused`; el nuevo dueño debe seguir exponiendo ese flag con la misma semántica. + +### Requirements +- Un único dueño del régimen de tiempo de juego (normal / cámara lenta / pausa) con precedencia definida. +- Exponer `game_time_paused` (== pausa) con semántica idéntica a la actual, para los consumidores ya Approved. +- Aplicar `offering_time_scale` uniformemente a los sistemas de tiempo de juego, con exenciones explícitas (reloj del beat, UI). +- Un contador `game_tick` **pause-aware y scale-aware** que la expiración de bendiciones (ADR-0001/F-RB4) lea, en vez de `Engine.get_physics_frames()`. +- El Temporizador consume este reloj compartido (cierra OQ-P4). +- Testeable: inyectable, sin RNG ni I/O en la lógica de régimen. + +## Decision + +**Se introduce un servicio Core `TimeControl` que posee el régimen canónico de tiempo de juego.** Es la única autoridad sobre "qué hora es en el juego" y sobre si el tiempo de juego está pausado o ralentizado. + +**Superficie:** +- `time_scale: float` — 1.0 normal, `offering_time_scale` (0.2) en cámara lenta, 0.0-efectivo en pausa. +- `game_time_paused: bool` (alias `is_paused`) — mismo flag y misma semántica que hoy expone Permadeath; los consumidores Approved (Combate AC-C52, Kaiju Regla 11, Input) ahora lo leen de `TimeControl`, no de Permadeath. +- `game_tick: int` — contador entero monótono de **tiempo de juego**: avanza a ritmo escalado y **se congela en pausa** (ver representación abajo). Es el reloj que la expiración de F-RB4/ADR-0001 lee. + +**Régimen por pila de precedencia (request/release).** Las fuentes **solicitan** un régimen; el efectivo es el de mayor precedencia activo: `PAUSED > SLOWED > NORMAL`. +- Permadeath solicita `PAUSED` al entrar un beat a `HOLDING`, lo libera al resolverse (única fuente de pausa en el MVP). +- Reliquias solicita `SLOWED` al abrir un `OFFERING`, lo libera al cerrar/cancelar (única fuente de cámara lenta en el MVP). +- Si una muerte ocurre durante un `OFFERING`: la solicitud `SLOWED` de la oferta **permanece registrada** pero queda **superada** por `PAUSED`; la oferta se preserva (su `offering_soft_timeout_s` lee `game_time`, que está congelado, así que no corre); al liberarse `PAUSED`, `SLOWED` vuelve a tomar efecto. Esto ratifica la precedencia de OQ-RB9 como contrato (cierra S6). + +**Consumo cooperativo (no engine-native).** Los sistemas de tiempo de juego avanzan su lógica leyendo `TimeControl` por polling: multiplican su delta por `time_scale` y/o comparan contra `game_tick`, y chequean `game_time_paused`. `TimeControl` **nunca** setea `SceneTree.paused` ni `Engine.time_scale`. Esto preserva el mecanismo cooperativo existente y permite eximir sistemas selectivamente. + +**Exenciones (corren en tiempo real, unscaled — NO leen `game_time`):** el reloj del beat `DEATH_HOLD` de Permadeath (`elapsed_s`, debe correr durante la pausa o deadlockea), la UI de la oferta y el HUD general. **El mecanismo de exención es simplemente que estos sistemas nunca sondean `TimeControl` — leen `_process(delta)`/tiempo real directamente.** (Nota: como esta decisión **nunca** setea `SceneTree.paused`, `PROCESS_MODE_ALWAYS` NO es lo que hace funcionar la exención; se deja como defensa documentada por si otro sistema introdujera `SceneTree.paused` en el futuro, no como el mecanismo.) + +**`game_tick` — entero vía acumulador escalado (preserva F-RB4).** La física corre a 60 tps fijos. Cada frame de física, `TimeControl` acumula `time_scale`; cuando el acumulador `≥ 1.0`, incrementa `game_tick` en 1 y resta 1.0. En pausa, `time_scale=0` no aporta al acumulador → `game_tick` se congela. Así `duration_ticks = ceil(duration_s × 60)` y `expiry_tick = applied_tick + duration_ticks` (F-RB4/ADR-0001) siguen siendo **enteros y deterministas**, pero medidos en tiempo de juego (se congelan en pausa, se ralentizan en la oferta) en vez de wall-clock. **Esto supersede el uso provisional de `Engine.get_physics_frames()` de ADR-0001.** + +**Alcance de la ralentización (decisión de diseño ratificada aquí):** la cámara lenta es uniforme sobre el tiempo de juego, así que una bendición de 20 s aplicada antes/durante una oferta dura 20 s de **tiempo de juego** (se ralentiza junto con todo lo demás), no 20 s de wall-clock. Coherente con "todo se ralentiza uniformemente" (Regla 7b) y con que `game_tick` es la base de la expiración. + +### Architecture Diagram + +``` + Fuentes de régimen (solicitan) TimeControl (Core, dueño del régimen) + ┌───────────────────────┐ ┌────────────────────────────────────┐ + │ Permadeath ──request_regime(PAUSED)──────► │ pila de precedencia: │ + │ (beat HOLDING) │ │ PAUSED > SLOWED > NORMAL │ + │ Reliquias ──request_regime(SLOWED)──────► │ effective = max(activos) │ + │ (OFFERING abierto) │ │ │ + └───────────────────────┘ │ expone: │ + │ time_scale: float │ + Consumidores (leen por polling) │ game_time_paused: bool │ + ┌───────────────────────────────────────────┐ │ game_tick: int (acumulador │ + │ Combate/Daño ── game_time_paused (AC-C52) │ │ escalado, congela en pausa) │ + │ Encuentro Kaiju ─ game_time_paused (R.11) │◄┤ │ + │ Input ─────────── game_time_paused │ │ request_regime(src, regime) │ + │ Temporizador ──── game_tick / time_scale │ │ release_regime(src) │ + │ (cierra OQ-P4) │ └────────────────────────────────────┘ + │ Combate/CombatState.sweep_expired(game_tick)│ + │ (expiración de bendiciones, ADR-0001) │ EXENTOS (tiempo real, no leen game_time): + └───────────────────────────────────────────┘ - reloj del beat DEATH_HOLD (Permadeath elapsed_s) + - UI de la oferta + HUD general +``` + +### Key Interfaces + +```gdscript +# TimeControl — servicio Core. Autoload que sostiene el reloj canónico, PERO inyectado +# por referencia (no llamado como TimeControl.* por nombre) para que los tests pinchen un +# reloj falso. Un reloj de juego canónico es un concern transversal legítimo; la inyección +# preserva la testabilidad que la regla DI-sobre-singletons del proyecto exige. +# INYECCIÓN: los consumidores reciben la referencia en UNA pasada de wiring (composition +# root) — p. ej. un setter desde el padre o un `NodePath` exportado. Prohibido `get_node( +# "/root/TimeControl")` disperso por el código de consumidor: eso reintroduce el acoplamiento +# global-por-nombre bajo apariencia de DI (grep de CI sobre esa ruta). +class_name TimeControl extends Node + +enum Regime { NORMAL = 0, SLOWED = 1, PAUSED = 2 } # valor entero = precedencia + +var time_scale: float # 1.0 / offering_time_scale / 0.0-en-pausa (read-only para consumidores) +var game_time_paused: bool # == régimen efectivo PAUSED; semántica idéntica a Permadeath R4 hoy +var game_tick: int # tiempo de juego entero; avanza escalado, congela en pausa + +func request_regime(source_id: StringName, regime: Regime) -> void # fuente pide un régimen +func release_regime(source_id: StringName) -> void # fuente lo suelta +# effective_regime = máx precedencia entre solicitudes activas; time_scale/paused derivan de él. +# offering_time_scale (0.2) es el time_scale del régimen SLOWED (parámetro, no hardcode). + +# Avance del reloj (interno, en _physics_process con paso fijo): +# _accum += time_scale # time_scale=0 en pausa → no avanza +# while _accum >= 1.0: game_tick += 1; _accum -= 1.0 +``` + +**Contrato de invariantes (loud-fail):** +- `0.0 < offering_time_scale < 1.0` (ya en Reliquias §Tuning Knobs; `TimeControl` lo valida al recibirlo). +- `game_tick` monótono no-decreciente; nunca retrocede. +- En pausa, `game_tick` no avanza y `time_scale == 0.0`. +- **El `_physics_process` de `TimeControl` corre incondicionalmente cada frame de física** (esta decisión nunca toca `SceneTree.paused`); el acumulador se congela en pausa **porque suma `time_scale == 0.0`**, no porque el nodo deje de procesar. (Sin esto, el reloj no podría "descongelarse" al liberar la pausa.) Como defensa (godot-specialist, revisión 2026-08-21) `TimeControl.process_mode = PROCESS_MODE_ALWAYS`, por si otro sistema introdujera `SceneTree.paused` en el futuro (no es el mecanismo, es cinturón-y-tirantes). +- **`TimeControl` debe avanzar `game_tick` ANTES de que cualquier consumidor lo lea en el mismo frame de física.** El orden de `_physics_process` entre nodos no está garantizado por Godot salvo por posición de árbol / `process_physics_priority`; hoy funciona solo por el artefacto de registro de Autoloads. Se fija explícitamente **`TimeControl.process_physics_priority = -1000`** para garantizar que corra primero sin importar el orden de árbol o de lista de Autoloads, más un test/assert de CI de que `game_tick` ya avanzó cuando un consumidor observa un frame dado (godot-specialist MEDIUM, revisión 2026-08-21). Una lectura de `game_tick` con 1 tick de retraso produce una expiración off-by-one silenciosa — inaceptable bajo la política loud-fail del proyecto. +- Una `source_id` no puede tener dos solicitudes activas simultáneas (re-request reemplaza). +- Consumidores de tiempo de juego **nunca** leen wall-clock/`_process(delta)` para lógica de simulación (grep CI). + +## Alternatives Considered + +### Alternative 1: Engine-native (`Engine.time_scale` + `SceneTree.paused`) +- **Description**: Cámara lenta vía `Engine.time_scale=0.2`; pausa vía `SceneTree.paused=true`. +- **Pros**: Cero código de reloj propio; el motor hace el trabajo. +- **Cons**: `SceneTree.paused` detiene **todos** los callbacks de proceso — el reloj del beat y la UI necesitarían `PROCESS_MODE_ALWAYS` por nodo, exactamente la gimnasia que los GDDs evitaron al elegir el flag cooperativo; `Engine.time_scale` escala la física global (incluida la parte que debe correr en tiempo real) e interactúa con la interpolación de física 3D rearquitecturada en 4.5; `Engine.get_physics_frames()` no es pause-aware (hueco heredado de ADR-0001). No soporta exención selectiva limpia. +- **Rejection Reason**: Contradice el mecanismo cooperativo ya elegido y la exención selectiva; reintroduce el hueco pause-aware. + +### Alternative 2: Sin dueño central — cada sistema se auto-coordina +- **Description**: Permadeath sigue dueño del flag de pausa, Reliquias dueño de la escala; cada sistema dependiente lee ambos directamente. +- **Pros**: Sin sistema nuevo. +- **Cons**: Es el estado roto actual — wiring N×M (cada sistema de tiempo debe conocer a Permadeath **y** a Reliquias), las filas de dependencia faltantes (F2-B3/A2), sin autoridad única de precedencia, y el Temporizador (OQ-P4) queda fuera. La "preservación de la oferta durante la pausa" habría que reimplementarla ad hoc. +- **Rejection Reason**: Exactamente lo que el review marcó como bloqueante; no escala a una tercera fuente de tiempo. + +### Alternative 3: El Temporizador de Preparación/Ritual lo posee +- **Description**: OQ-RB9 sugiere que el Temporizador "ya posee el tiempo de fase"; extenderlo para poseer pausa + cámara lenta globales. +- **Pros**: Reutiliza un dueño de tiempo existente. +- **Cons**: El Temporizador posee el **reloj de acercamiento de la era** (un concepto de dominio Gameplay), no el régimen global de tiempo. Sobrecargarlo haría que Permadeath (Legado) y Reliquias (Legado) dependieran de un sistema Gameplay-Feature para pausar/ralentizar — inversión de capas. Además el reloj de acercamiento se detiene en `CLIMAX`, justo donde vive la cámara lenta de la oferta. +- **Rejection Reason**: Inversión de capas y solapamiento de responsabilidades; el régimen global es Core, no Gameplay. + +## Consequences + +### Positive +- Un único dueño y una única precedencia para el tiempo de juego; F2-B4/S2 cerrado. +- `game_time_paused` sobrevive con semántica idéntica — Combate/Kaiju/Input no cambian su contrato, solo la fuente de lectura (Permadeath → TimeControl). El wiring huérfano Permadeath↔Reliquias se resuelve haciéndolos ambos clientes del hub. +- OQ-P4/F2-W4 cerrado: el Temporizador consume el reloj compartido y se congela/ralentiza gratis. +- OQ-RB9/S6 ratificado como contrato: pausa > cámara lenta, oferta preservada. +- La expiración de bendiciones (ADR-0001/F-RB4) queda pause-aware y scale-aware sin lógica cross-sistema; cierra el hand-off que ADR-0001 dejó abierto. +- Extensible: una tercera fuente de tiempo futura (p. ej. bullet-time de otra mecánica) solo solicita un régimen en la pila. + +### Negative +- Sistema Core nuevo que **no** estaba en el systems-index (cambio de alcance de infraestructura — añadir como sistema/servicio de soporte). +- Todo sistema de tiempo de juego gana una dependencia a `TimeControl` (aunque reemplaza dependencias directas a Permadeath/Reliquias, así que a menudo es neutro o más limpio). +- Disciplina requerida: cualquier lógica de simulación que use `_process(delta)` wall-clock es un bug; hay que enforcalo por revisión/CI. + +### Risks +- **Autoload vs DI**: `TimeControl` como Autoload roza la regla "DI sobre singletons". *Mitigación*: sostener el reloj en el Autoload pero **inyectar la referencia** (no `TimeControl.` por nombre); los tests pinchan un `FakeClock` con el mismo interfaz. Registrar la excepción explícita (concern transversal legítimo, como un bus de eventos) en el control-manifest. +- **Sistema que olvida leer `time_scale`/`game_time_paused`** y corre en wall-clock → se desincroniza de la pausa/cámara lenta. *Mitigación*: grep de CI sobre `_process(delta)` en scripts de simulación; checklist en el control-manifest de qué sistemas son de tiempo de juego vs exentos. +- **Suavidad visual bajo cámara lenta**: escalar el avance de lógica sin tocar `physics_ticks_per_second` mantiene un tick real por frame; conviene verificar suavidad visual bajo cámara lenta (Verification Required 1). La rearquitectura de interpolación de la breaking-changes L38 es **solo de física 3D**; la 2D del MVP no cambia en 4.6 (current-best-practices L35), así que no hay riesgo de migración aquí — solo una verificación de feel. +- **Fuga entre régimen y expiración**: si `game_tick` avanzara fraccional o no-monótono, la expiración se rompe. *Mitigación*: acumulador entero con invariante de monotonía loud-fail. +- **Semántica de duración en cámara lenta**: se decide que las bendiciones duran tiempo de **juego** (se ralentizan). Si playtest mostrara que se siente mal (un buff que "dura demasiado" en wall-clock durante una oferta), es un cambio de política acotado a cómo `game_tick` alimenta la expiración — no un rediseño. *Registrado para validación en vertical slice.* + +## GDD Requirements Addressed + +| GDD System | Requirement | How This ADR Addresses It | +|------------|-------------|--------------------------| +| `permadeath.md` | Regla 4 `game_time_paused` cooperativo (no `SceneTree.paused`); reloj del beat corre unscaled; OQ-P4 (¿la pausa alcanza al Temporizador?) | Permadeath solicita `PAUSED`; `TimeControl` expone `game_time_paused` con semántica idéntica; el reloj del beat queda exento (tiempo real); el Temporizador ahora lee el reloj compartido → OQ-P4 cerrado | +| `reliquias-bendiciones.md` | Regla 7b `offering_time_scale` uniforme; OQ-RB9 (dueño del reloj + precedencia pausa>cámara-lenta + preservar la oferta) | Reliquias solicita `SLOWED` con `offering_time_scale`; `TimeControl` lo aplica uniformemente; precedencia `PAUSED>SLOWED` ratificada; la oferta se preserva porque su soft-timeout lee `game_time` congelado | +| `reliquias-bendiciones.md` / ADR-0001 | F-RB4 expiración por tick determinista, pause-aware | `game_tick` entero, scale-aware y pause-aware; `sweep_expired(game_tick)` de ADR-0001 lo lee; supersede `Engine.get_physics_frames()` | +| `temporizador-de-preparacion-ritual.md` | El reloj de acercamiento/fase debe congelarse en un beat de muerte y ralentizarse en una oferta (hoy solo conoce el menú de pausa global, AC-T22) | El Temporizador consume `TimeControl.game_tick`/`time_scale` en vez de su wall-clock; F2-W4 cerrado (edición cross-GDD a aplicar al aceptar) | +| `combate-dano.md` / `encuentro-con-kaiju.md` | Combate AC-C52 y Kaiju Regla 11/AC-K89 leen `game_time_paused` | Siguen leyendo el mismo flag, ahora desde `TimeControl` (fuente de lectura cambia, semántica no) | + +## Performance Implications +- **CPU**: `TimeControl` hace O(1) por frame de física (acumulador + resolución de precedencia sobre un puñado de solicitudes). Los consumidores hacen una lectura de flag/escala. Despreciable. +- **Memory**: Un servicio + una pila de solicitudes pequeña. Trivial. +- **Load Time**: Nulo. +- **Network**: N/A (sin multijugador en el MVP). + +## Migration Plan +No hay código aún. En implementación: (1) crear `TimeControl` (Autoload + interfaz inyectable + `FakeClock` de test). (2) Permadeath deja de exponer `game_time_paused` como dueño y pasa a **solicitar** `PAUSED`; su reloj de beat se marca exento. (3) Reliquias solicita `SLOWED`. (4) Combate/Kaiju/Input cambian su lectura de `game_time_paused` de Permadeath a `TimeControl` (semántica idéntica — cambio mecánico). (5) Temporizador consume `game_tick`/`time_scale`. (6) La expiración de ADR-0001 se ancla a `TimeControl.game_tick`. **Ediciones cross-GDD a aplicar al aceptar**: `permadeath.md` (OQ-P4 cerrado, fuente de `game_time_paused` movida a TimeControl), `reliquias-bendiciones.md` (OQ-RB9 cerrado, dueño del reloj = TimeControl), `temporizador-de-preparacion-ritual.md` (consume el reloj compartido), nota en `combate-dano.md`/`encuentro-con-kaiju.md` (fuente de lectura). Añadir `TimeControl` al systems-index como servicio Core de soporte. + +## Validation Criteria +- Test unitario: sin solicitudes → régimen NORMAL, `time_scale==1.0`, `game_tick` avanza 1/tick de física. +- Test unitario: `request_regime(reliquias, SLOWED)` → `time_scale==0.2`, `game_tick` avanza ~1 cada 5 ticks de física. +- Test unitario: `request_regime(permadeath, PAUSED)` con SLOWED activo → efectivo PAUSED, `game_time_paused==true`, `game_tick` congelado; al `release_regime(permadeath)` → vuelve a SLOWED (oferta preservada). +- Test unitario: `game_tick` monótono no-decreciente en toda transición de régimen. +- Test de integración: una bendición de 20 s aplicada, luego una oferta abierta 5 s (wall-clock) → la expiración se corre en tiempo de juego (dura 20 s de juego, no 20 s de wall-clock). +- Test de integración: muerte durante una oferta → la oferta no cancela ni corre su soft-timeout durante el beat; reanuda cámara lenta al terminar (cierra S6). +- CI: grep de `_process(delta)` en scripts de simulación de tiempo de juego → cero (deben usar `TimeControl`). +- Loud-fail: `offering_time_scale ∉ (0,1)`, `game_tick` no-monótono → error ruidoso. + +## Related Decisions +- ADR-0001 (capa de modificadores de stat por instancia) — este ADR fija la fuente de tick pause-aware que ADR-0001 difirió; `sweep_expired`/`expiry_tick` se anclan a `TimeControl.game_tick`. +- **ADR-0003** (persistencia de Guardado — `docs/architecture/adr-0003-save-persistence-serialization.md`, Proposed): el rebasing de `expiry_tick` en recarga se define contra `TimeControl.game_tick` — se persiste `remaining_ticks` (relativo) y se rebasa al cargar (fresh `game_tick=0` + `remaining_ticks`). Prohíbe persistir `game_tick`/`expiry_tick` absolutos (forbidden pattern `persisting_absolute_game_tick`). Cierra el hand-off de persistencia (F2-B7/S4). +- `design/gdd/gdd-cross-review-2026-08-18.md` — F2-B4/S2 (origen), OQ-P4/F2-W4, OQ-RB9/S6. +- GDDs a reconciliar al aceptar: `permadeath.md`, `reliquias-bendiciones.md`, `temporizador-de-preparacion-ritual.md`, `combate-dano.md`, `encuentro-con-kaiju.md`. diff --git a/docs/architecture/adr-0003-save-persistence-serialization.md b/docs/architecture/adr-0003-save-persistence-serialization.md new file mode 100644 index 0000000000..2d80cb47c5 --- /dev/null +++ b/docs/architecture/adr-0003-save-persistence-serialization.md @@ -0,0 +1,262 @@ +# ADR-0003: Serialización y contrato de runtime de Guardado/Persistencia + +## Status +Proposed + +## Date +2026-08-21 + +## Engine Compatibility + +| Field | Value | +|-------|-------| +| **Engine** | Godot 4.6 | +| **Domain** | Core (I/O de archivos / serialización) | +| **Knowledge Risk** | MEDIUM | +| **References Consulted** | `docs/engine-reference/godot/VERSION.md`; `docs/engine-reference/godot/breaking-changes.md` (4.3→4.4: `FileAccess.store_*` — incl. `store_var` — pasan de `void` a devolver `bool`); `docs/engine-reference/godot/deprecated-apis.md` (sin entradas de I/O de archivos o serialización) | +| **Post-Cutoff APIs Used** | `FileAccess.store_var`/`get_var` con su valor de retorno `bool` (post-4.4) usado como señal de fallo de escritura de primera clase — mi training data pre-4.4 asumiría `void` y necesitaría un mecanismo distinto (try/reopen) para detectar el mismo fallo | +| **Verification Required** | (1) Confirmar en 4.6 que `store_var`/`store_buffer` devuelven `false` de forma fiable ante un fallo real de disco a mitad de escritura (no solo ante errores triviales de argumento). (2) Confirmar que `hash()` (Variant hash) es determinista dentro de una misma versión de motor para el mismo `Dictionary` de payload — no se requiere estabilidad *entre* versiones de motor, ver Decision/Risks. (3) `Engine.physics_ticks_per_second == 60` ya se assertea por ADR-0001/ADR-0002; este ADR no añade un nuevo assert, pero el rebasing de `expiry_tick` (ver Decision) depende de ese supuesto seguir siendo cierto. (4) **Semántica de rename sobre archivo existente en el target Windows del proyecto** (godot-specialist, revisión 2026-08-21): POSIX `rename()` reemplaza el destino atómicamente, pero las semánticas de rename-sobre-archivo-existente de Windows han tenido históricamente más aristas; la referencia de motor no confirma cómo se comporta `DirAccess.rename()` cross-platform en 4.6. Probar manualmente en el target Windows real que el rename `save.tmp`→`save` reemplaza atómicamente el archivo existente (garantía de AC-27) antes de confiar en ello. | + +## ADR Dependencies + +| Field | Value | +|-------|-------| +| **Depends On** | ADR-0001 (Accepted) — forma de `StatModifier`/`expiry_tick`/`source_relic_id`; ADR-0002 (Accepted) — `TimeControl.game_tick` como fuente de tick pause-aware/scale-aware que este ADR debe rebasear entre sesiones | +| **Enables** | El futuro ADR de Forja de Legado (OQ-FL4 — representación de `RelicRecord`, Resource vs dato serializado) puede apoyarse en la API de serialización elegida aquí sin reabrirla; epics de Salón Conmemorativo/Panteón (#14) y Transición de Era (#15), ambos dependientes duros de Guardado/Persistencia | +| **Blocks** | Epic de Guardado/Persistencia (#3, Foundation) no puede empezar implementación hasta que este ADR sea Accepted; Reliquias/Bendiciones AC-RB46 (bloqueada, OQ-RB2); Forja de Legado AC-FL11 (round-trip de `forge_counter`); Permadeath (recuperación por crash del payload de muerte pendiente, AC-H27c) | +| **Ordering Note** | Tercer ADR del proyecto. Ambos ADRs previos (Accepted 2026-08-21) **difieren explícitamente** a este — bloqueante de arquitectura **S4** de la revisión cross-GDD 2026-08-18/21, el gap de Foundation más urgente según `/architecture-review`. | + +## Context + +### Problem Statement + +`guardado-persistencia.md` ya fija las **reglas** del sistema de guardado (Core Rules 1–7, Approved-ready en diseño): slot único, escritura atómica vía `save.tmp` + rename, `schema_version` con cadena de migración lineal, checksum de corrupción sin auto-reparación. Pero dos decisiones de implementación quedan explícitamente abiertas en el propio GDD (tabla de Open Questions) y **ambos ADRs previos difieren aquí de forma explícita**: + +- **ADR-0001** difiere la persistencia del estado efímero de invocación y el rebasing de `expiry_tick` en recarga (Related Decisions). +- **ADR-0002** difiere que el rebasing de `expiry_tick` se defina contra `TimeControl.game_tick`, no contra un contador de motor — pero `game_tick` es un contador **por sesión** (arranca en 0 con `TimeControl`, un `Node`/Autoload que se recrea en cada carga), así que un `expiry_tick` absoluto persistido de una sesión anterior no significa nada frente a un `game_tick` que reinició a 0. +- **Forja de Legado (OQ-FL4, punto 3 del scope obligatorio del ADR)** exige que `forge_counter` (contador monótono de índice de forja) sobreviva como **`int64` real** — JSON trunca enteros a 53 bits de precisión (double IEEE 754), lo cual rompe silenciosamente el contrato de unicidad de `relic_id` en legados muy largos. +- **Reliquias/Bendiciones (OQ-RB2, AC-RB46, bloqueada)** necesita que `invocation_charges_remaining` y la lista de `StatModifier` activos (con su tiempo de expiración restante) se restauren correctamente en una recarga a mitad de era. +- **Sistema de Héroes (AC-H26/AC-H27c)** persiste un payload de muerte marcado "pendiente de sellar" para que el flujo de carga pueda coaccionar `DYING→DEAD` si el juego crashea a mitad del beat `DEATH_HOLD` de Permadeath. +- El propio GDD deja abierto **qué algoritmo de checksum usar** (tabla de Open Questions: "no necesita ser criptográficamente seguro"). + +Ninguna de estas piezas cambia las *reglas* ya fijadas en `guardado-persistencia.md` — todas son la mecánica concreta de *cómo* se serializa, se verifica, y se rebasa el estado entre sesiones. Esto es lo que este ADR fija. + +### Constraints + +- **Las reglas del GDD ya están fijadas y no se reabren aquí**: slot único (Regla 3), persistencia por referencia (Regla 4), escritura atómica `save.tmp`→rename (Regla 5), versionado de esquema con migración lineal v→v+1 (Regla 6), checksum sin auto-reparación (Regla 7). Este ADR es puramente el *mecanismo* bajo esas reglas. +- **Fidelidad `int64` obligatoria** para `forge_counter` (Forja OQ-FL4 pt.3) — cualquier formato que pase por un `double` de 53 bits de mantisa es inaceptable como contrato general de serialización. +- **`game_tick` es un contador por sesión**, no un reloj persistente — ADR-0002 lo diseñó para congelarse en pausa y escalar con `time_scale`, no para sobrevivir cierres de proceso. Rebasear `expiry_tick` contra él implica que el propio `game_tick` no puede ser el dato persistido. +- **DI sobre singletons** (coding-standards) — el mecanismo de guardado debe ser testeable sin tocar disco real (inyectar una ruta de archivo temporal o una capa de I/O simulable) y sin depender de `TimeControl` por nombre global. +- **Loud-fail sobre clamp silencioso** (precedente Héroes/Combate/ADR-0001/ADR-0002) — versión futura, checksum inválido, y fallo de migración deben fallar ruidosamente, nunca degradarse en silencio. +- **Sin lógica de juego en la capa de guardado** (Overview del GDD) — el mecanismo elegido debe mantener el guardado como infraestructura opaca: no necesita entender *qué significa* cada campo, solo transportarlo con fidelidad. + +### Requirements + +- Un formato de serialización único que preserve `int64` exacto. +- Un checksum calculado sobre el payload serializado, sin requerir librería externa. +- Una envoltura (`schema_version` + payload) con migración lineal v→v+1, ejecutable sobre datos aún no deserializados a tipos fuertes. +- Un contrato de round-trip para el estado efímero de invocación (cargas + modificadores activos) que sobreviva la recarga sin depender de que `game_tick` conserve su valor entre sesiones. +- Un contrato de round-trip para el payload de muerte pendiente de sellar (Héroes/Permadeath). +- Testeable sin escena completa: inyección de ruta de archivo o mock de la capa de I/O, sin RNG. + +## Decision + +**Se introduce un servicio Core `SaveService` que posee el mecanismo de serialización, el envoltorio versionado, el checksum, y la orquestación de escritura atómica — pero no conoce el significado de ningún campo de dominio.** Cada sistema dependiente (Forja, Reliquias, Temporizador, Datos de Era, Héroes/Permadeath, Transición de Era) implementa un contrato simétrico `serialize_state() -> Dictionary` / `deserialize_state(data: Dictionary) -> void`; `SaveService` ensambla estos sub-diccionarios bajo una clave por sistema en un único payload raíz, sin inspeccionar su contenido. + +**Formato de serialización: `FileAccess.store_var` / `get_var` (binario nativo de Godot) sobre un `Dictionary` anidado.** Se descarta JSON como formato general — `JSON.stringify`/`parse` representa todo número como `double` de 53 bits de mantisa, lo cual truncaría silenciosamente `forge_counter` en legados largos (el escenario exacto que OQ-FL4 punto 3 marca como obligatorio de evitar). `store_var`/`get_var` preserva `int` de Godot (64 bits) sin conversión ni codificación especial, para todo campo de todo sistema — no solo para `forge_counter` — eliminando la necesidad de una regla especial por campo que un futuro sistema podría olvidar aplicar. + +**Checksum: `hash()` (Variant hash) de Godot sobre el `Dictionary` del payload, calculado antes de escribir y comparado tras recalcularlo al cargar.** El GDD (Edge Cases / Open Questions) es explícito en que el checksum defiende contra corrupción y escrituras parciales, **no** contra manipulación adversarial — no necesita ser criptográficamente fuerte ni estable entre versiones de motor (el guardado no está pensado para portarse entre versiones de Godot; una migración de motor sería, en todo caso, un evento manual y auditado, fuera del alcance de este mecanismo). Usar la función integrada evita añadir una implementación manual de CRC32 para un requisito que el propio diseño ya acotó como más débil. + +**Escritura atómica usando el retorno `bool` de `store_var`/`store_buffer` (post-4.4) como señal temprana de fallo.** Cada llamada de escritura a `save.tmp` revisa su valor de retorno; un `false` en cualquier paso aborta la escritura inmediatamente y dispara el camino de reintento de `retry_delay_ms` (ya fijado en el GDD) **sin** intentar el rename — el archivo final permanece intacto (Regla 5 / AC-25/AC-26). El rename atómico en sí (`DirAccess.rename` o equivalente) es la única operación que puede dejar un estado ambiguo si falla a mitad de camino (AC-27), consistente con el GDD. + +**Rebasing de `expiry_tick`: se persiste duración restante relativa, nunca el tick absoluto ni `game_tick` mismo.** `TimeControl.game_tick` (ADR-0002) **no se persiste en absoluto** — cada sesión arranca `game_tick` en 0 por diseño, congruente con su propósito (contador de tiempo de juego por sesión, pause-aware/scale-aware, no un reloj de pared). Para cada `StatModifier` activo, `SaveService`/Reliquias persiste `remaining_ticks = expiry_tick − game_tick` calculado **en el momento del guardado**. Al cargar, cada modificador se re-añade con `expiry_tick = game_tick_al_cargar (0, sesión fresca) + remaining_ticks`. Esto desacopla por completo el formato de guardado de cuánto dure una sesión — nunca hay que razonar sobre "cuánto tiempo real pasó con el juego cerrado", que es información que el diseño (Regla: las bendiciones duran tiempo de *juego*, ADR-0002) nunca quiso modelar. + +**Payload de muerte pendiente de sellar (Héroes AC-H26/Permadeath AC-H27c):** se persiste como un sub-diccionario opaco más bajo la clave del Sistema de Héroes, con un campo explícito `sealed: bool = false` hasta que Permadeath complete el beat `DEATH_HOLD` y confirme `DEAD`. Al cargar con `sealed == false`, el flujo de carga invoca el mismo camino de coacción `DYING→DEAD` que ya usaría un crash en vivo (contrato ya fijado en `sistema-de-heroes.md`/`permadeath.md` — este ADR no lo redefine, solo confirma que viaja intacto en el payload serializado). + +**Migraciones sobre `Dictionary` crudo, antes de deserializar a tipos fuertes.** Cada migración es una función pura `migrate_vN_to_vNplus1(data: Dictionary) -> Dictionary` registrada en una tabla ordenada por versión; `SaveService` las aplica secuencialmente desde `save.schema_version` hasta `CURRENT_SCHEMA_VERSION` **antes** de invocar `deserialize_state` de ningún sistema — los sistemas dependientes nunca ven un esquema viejo. + +### Architecture Diagram + +``` + Sistemas dependientes (contrato simétrico, opaco para SaveService) + ┌─────────────────────────────────────────────────────────────┐ + │ Forja de Legado ─────► serialize_state() -> {forge_counter, │ + │ relic_records: [...]} │ + │ Reliquias/Bendiciones ► serialize_state() -> {charges, │ + │ active_modifiers: [{stat, magnitude, │ + │ remaining_ticks, source_relic_id}]} │ + │ Sistema de Héroes ────► serialize_state() -> {pending_death: │ + │ {..., sealed: bool}} │ + │ Temporizador ─────────► serialize_state() -> {elapsed_s, │ + │ current_phase} │ + │ Datos de Era/Civ. ────► serialize_state() -> {era_id activa} │ + │ Transición de Era ────► serialize_state() -> {era_id/ARCHIVED}│ + └─────────────────────────────────────────────────────────────┘ + ▲ deserialize_state(data) │ serialize_state() + │ (tras migraciones) ▼ + ┌─────────────────────────────────────────────────────────────┐ + │ SaveService (Core) — NO conoce el significado de los campos │ + │ │ + │ payload = { schema_version, sistemas: { : } } │ + │ checksum = hash(payload) │ + │ │ + │ request_save(): │ + │ store_var(save.tmp, payload+checksum) -> bool en cada paso│ + │ bool==false en cualquier paso → retry_delay_ms, sin rename│ + │ éxito → rename atómico save.tmp → save │ + │ │ + │ request_load(): │ + │ get_var(save) -> payload; recompute hash; comparar │ + │ mismatch → CORRUPTED (sin auto-reparación) │ + │ payload.schema_version > CURRENT → bloqueo explícito │ + │ payload.schema_version < CURRENT → migrar v→v+1 en cadena │ + │ despachar sub-dict a cada sistema.deserialize_state() │ + └─────────────────────────────────────────────────────────────┘ + │ rebasing de expiry_tick (Reliquias, al deserializar): + ▼ expiry_tick_nuevo = TimeControl.game_tick (0, sesión fresca) + + remaining_ticks_persistido +``` + +### Key Interfaces + +```gdscript +# SaveService — servicio Core. Inyectado por referencia (composition root), igual que +# TimeControl (ADR-0002) — NO se llama SaveService.* por nombre global disperso. +# Los tests inyectan una ruta de archivo temporal (o un FileAccess-like fake) para +# evitar tocar disco real. +class_name SaveService extends Node + +const CURRENT_SCHEMA_VERSION: int = 1 +const SAVE_PATH: String = "user://save.dat" +const SAVE_TMP_PATH: String = "user://save.tmp" + +enum LoadResult { NO_SAVE_EXISTS, SAVE_EXISTS, CORRUPTED, VERSION_TOO_NEW } + +# Contrato simétrico que cada sistema dependiente implementa (no un método de SaveService): +# func serialize_state() -> Dictionary +# func deserialize_state(data: Dictionary) -> void +# SaveService los invoca por una lista de referencias inyectadas (composition root), +# nunca por búsqueda de nodo/autoload por nombre. + +func register_persistable(system_key: StringName, node: Node) -> void +# node debe implementar serialize_state()/deserialize_state(Dictionary); loud-fail si no. + +func request_save() -> bool +# Ensambla payload = {schema_version: CURRENT_SCHEMA_VERSION, sistemas: {...}}, +# calcula checksum = hash(payload), y escribe atómicamente el sobre externo +# {payload: payload, checksum: checksum} (checksum como clave HERMANA, no dentro +# del dict hasheado). Abre save.tmp con FileAccess.open(WRITE) y chequea +# get_open_error() ANTES de store_var; un open fallido o un store_var()==false +# aborta sin rename y entra al reintento retry_delay_ms (GDD Formulas). Serializa el +# estado de invocación de Reliquias como remaining_ticks (ver rebasing en Decision), +# NUNCA como expiry_tick absoluto ni como TimeControl.game_tick. Retorna éxito/fallo. + +func request_load() -> LoadResult +# FileAccess.open(SAVE_PATH, READ) + get_open_error() (si falla la apertura y no hay +# archivo -> NO_SAVE_EXISTS; si existe pero no abre -> CORRUPTED). get_var(allow_objects +# = false) del sobre; recalcula hash(sobre.payload), compara con sobre.checksum. Si +# payload.schema_version > CURRENT_SCHEMA_VERSION -> VERSION_TOO_NEW (bloqueo, sin +# migración, ver Edge Cases del GDD). Si < CURRENT -> aplica la cadena de migraciones +# registrada, en orden, v->v+1 exclusivamente. Despacha cada sub-dict a +# deserialize_state() del sistema correspondiente. Detecta y descarta cualquier +# save.tmp huérfano antes de leer save. + +# Migraciones: tabla ordenada de funciones puras sobre Dictionary crudo (pre-tipos fuertes). +# func migrate_v1_to_v2(data: Dictionary) -> Dictionary # ejemplo de forma, v2 aún no existe +``` + +**Contrato de invariantes (loud-fail):** +- `forge_counter` (y cualquier campo declarado `int64` por un sistema dependiente) se serializa con `store_var`, nunca convertido a `float`/`String` en el camino — un intento de pasar por JSON en cualquier sub-sistema es un bug de programación. +- `TimeControl.game_tick` **nunca** aparece en el payload serializado de ningún sistema — solo `remaining_ticks` (relativo). Un campo `game_tick`/`expiry_tick` absoluto en un `serialize_state()` es un error de diseño de ese sistema, detectable por revisión/CI (grep del símbolo `game_tick` fuera de `time_control.gd`). +- **`serialize_state()` devuelve únicamente `Variant` planos: `Dictionary`/`Array`/primitivos — nunca instancias `RefCounted`/`Resource`/`Object`** (p. ej. un `StatModifier` vivo). `store_var`/`get_var` mantienen sus parámetros `full_objects`/`allow_objects` en su default `false`: deserializar Objetos desde un archivo es superficie de ataque (instanciación arbitraria desde un save manipulado), no solo estilo. Los sistemas convierten su estado a `Dictionary` plano **antes** de entregarlo a `SaveService` (grep CI: ningún `store_var(..., true)`/`get_var(..., true)` en el código de guardado). (godot-specialist, revisión 2026-08-21.) +- **El handle de `FileAccess` se valida antes de cualquier `store_var`/`get_var`.** El retorno `bool` de `store_var` solo cubre fallos *con un handle ya abierto*; si `FileAccess.open(SAVE_TMP_PATH, WRITE)` falla (disco lleno, permisos, `user://` no escribible), devuelve `null` y llamar `.store_var()` sobre él crashea. `request_save`/`request_load` chequean `FileAccess.get_open_error()` inmediatamente tras cada `open()`, antes de tocar el archivo — un `open()` fallido entra al camino de reintento de `retry_delay_ms` (escritura) o a `CORRUPTED`/`NO_SAVE_EXISTS` (lectura), nunca a un crash de referencia nula. (godot-specialist, revisión 2026-08-21.) +- El checksum se calcula **después** de que todos los `serialize_state()` completaron y **antes** de escribir a `save.tmp` — nunca sobre datos parcialmente ensamblados. **Envoltura explícita**: el payload se escribe como `{ payload: {schema_version, sistemas:{...}}, checksum: int }` — el `checksum` es una clave **hermana externa** al `Dictionary` que se hashea, no una clave insertada dentro de él (evita el riesgo de hashear un dict que ya contiene un `checksum: 0` placeholder). Al cargar, se recalcula `hash(payload)` y se compara con `checksum`. (godot-specialist, revisión 2026-08-21.) +- Una migración nunca salta versiones (v→v+2 directo prohibido, ya fijado en el GDD Regla 6 — este ADR no lo cambia, solo fija que las migraciones operan sobre `Dictionary` crudo, no sobre instancias ya deserializadas a clases tipadas). Nota: `get_var` no reconstruye el *tipado estático* de un `Array[T]`/`Dictionary` tipado — por eso los sistemas entregan `Array`/`Dictionary` planos (p. ej. `active_modifiers` como `Array` de dicts sin tipar), congruente con que las migraciones operan sobre `Dictionary` crudo. +- `sealed: bool` del payload de muerte pendiente nunca se omite — un sistema que no lo incluya rompe el contrato de recuperación por crash (AC-H27c) de forma silenciosa. + +## Alternatives Considered + +### Alternative 1: JSON (`store_string` + `JSON.stringify`/`parse`) +- **Description**: Serializar el payload como texto JSON legible por humanos. +- **Pros**: Diffable, editable a mano durante desarrollo, inspeccionable sin abrir Godot. +- **Cons**: Todo número pasa por `double` (53 bits de mantisa) — `forge_counter` se trunca silenciosamente en legados largos, violando el requisito explícito de OQ-FL4 punto 3. Requeriría una codificación especial (p. ej. campo como `String`) solo para ese campo, con el riesgo de que un futuro campo `int64` (p. ej. un contador similar en otro sistema) olvide aplicar la misma regla especial. +- **Rejection Reason**: Rompe un requisito ya explícito de un GDD Approved (Forja de Legado); la legibilidad no compensa un bug de truncamiento silencioso en un contador que garantiza unicidad de `relic_id`. + +### Alternative 2: Híbrido — JSON con campos `int64` codificados como `String` +- **Description**: Mantener JSON para la mayoría de campos (legibilidad), pero serializar cualquier campo `int64` como texto y parsearlo de vuelta a `int` al cargar. +- **Pros**: Conserva la inspeccionabilidad de JSON para la mayoría del guardado. +- **Cons**: Introduce una regla asimétrica por tipo de campo que cada sistema dependiente debe recordar aplicar correctamente — exactamente el tipo de disciplina manual perpetua que el proyecto prefiere evitar (precedente: "loud-fail sobre disciplina manual" en ADR-0001/0002). Un futuro sistema con su propio contador de 64 bits podría usar un `int` JSON normal por descuido y fallar solo en producción, tras muchas horas de juego — el peor momento para un truncamiento silencioso. +- **Rejection Reason**: Añade complejidad y una fuente de error recurrente y sistémica para resolver un problema que `store_var` resuelve uniformemente sin reglas especiales. + +### Alternative 3: Persistir `TimeControl.game_tick` absoluto entre sesiones (en vez de rebasing relativo) +- **Description**: Guardar el valor de `game_tick` junto con `expiry_tick` absoluto de cada modificador; al cargar, restaurar `game_tick` al valor persistido en vez de reiniciar en 0. +- **Pros**: No requiere una operación de rebasing explícita — los ticks absolutos siguen siendo válidos si `game_tick` continúa donde quedó. +- **Cons**: Convierte `game_tick` en un reloj de pared disfrazado que sobrevive cierres de proceso — exactamente lo que ADR-0002 evitó al diseñarlo como contador de tiempo de *juego* por sesión (congela en pausa, escala con `time_scale`, sin semántica definida para "cuánto tiempo real pasó con el juego cerrado"). Acopla el formato de guardado a la vida útil de `TimeControl`, y arriesga overflow/drift en legados de sesiones muy largas sin ningún beneficio de diseño — el GDD nunca pidió que las bendiciones "seguende corriendo" mientras el juego está cerrado. +- **Rejection Reason**: Contradice el propósito de diseño de `game_tick` fijado en ADR-0002; el rebasing relativo (Decision) logra el mismo resultado observable sin la deuda conceptual. + +### Alternative 4: CRC32 manual para el checksum +- **Description**: Implementar CRC32 sobre los bytes del payload serializado. +- **Pros**: Algoritmo estándar, ampliamente entendido, portable entre implementaciones. +- **Cons**: Godot no expone un CRC32 de propósito general integrado para `PackedByteArray` arbitrario — requeriría una implementación manual (tabla de lookup o cálculo bit a bit) para un requisito que el propio GDD (Open Questions) ya acota como "no necesita ser criptográficamente segura, defiende contra corrupción/escrituras parciales, no contra manipulación". +- **Rejection Reason**: Código adicional sin beneficio medible frente a `hash()` integrado, dado el requisito real (detectar corrupción/escritura parcial, no resistir manipulación adversarial ni portarse entre motores). + +## Consequences + +### Positive +- Cierra el bloqueante de arquitectura **S4**: los tres huecos que ambos ADRs previos dejaron abiertos (formato de serialización, checksum, rebasing de `expiry_tick`) quedan resueltos con una sola decisión coherente. +- `forge_counter` (Forja AC-FL11) y cualquier futuro contador `int64` se serializan sin riesgo de truncamiento, sin reglas especiales por campo. +- El rebasing relativo de `expiry_tick` desbloquea Reliquias AC-RB46 sin que `TimeControl` tenga que cambiar su diseño (ADR-0002 permanece intacto — `game_tick` sigue siendo puramente por sesión). +- El contrato `serialize_state()`/`deserialize_state()` mantiene a `SaveService` genuinamente agnóstico de dominio (Overview del GDD: "no contiene lógica de juego") — añadir un nuevo sistema persistible (Transición de Era, Panteón) no requiere tocar `SaveService`, solo implementar el contrato. +- El payload de muerte pendiente (`sealed: bool`) hace explícito y testeable el camino de recuperación por crash que Héroes/Permadeath ya diseñaron (AC-H27c), en vez de dejarlo implícito en la forma del dato. + +### Negative +- `store_var`/`get_var` produce un archivo binario no legible por humanos — depurar un guardado de un jugador reportando un bug requiere abrir el proyecto en Godot (o un script standalone) en vez de leer un archivo de texto directamente. +- El contrato simétrico `serialize_state()`/`deserialize_state()` es disciplina perpetua: cada sistema dependiente nuevo (Panteón, Transición de Era, y cualquier sistema futuro) debe implementarlo correctamente, incluyendo la regla `remaining_ticks`-nunca-`game_tick`/`expiry_tick`-absoluto para cualquier duración temporizada que introduzca. +- Las migraciones sobre `Dictionary` crudo (antes de tipos fuertes) significan que el código de migración no tiene la seguridad de tipos del resto del proyecto — un error de clave (`data.get("froge_counter")`, typo) solo se detecta en tiempo de ejecución, no en compilación. + +### Risks +- **Colisión de `hash()`**: `hash()` de Godot devuelve un hash de **32 bits** (extendido con ceros al `int` de 64 bits de GDScript), no un hash de 64 bits ni criptográfico — la probabilidad de colisión ante un payload corrupto es del orden de 1-en-~4·10⁹, no "astronómica" (godot-specialist, revisión 2026-08-21). Sigue siendo aceptable dado el requisito real del GDD (detectar corrupción/escritura parcial, no resistir manipulación), pero un lector futuro no debe sobre-confiar en el ancho de bits. *Mitigación*: ninguna requerida más allá de lo ya aceptado en el GDD; si algún día se quisiera más margen, el algoritmo de checksum es un detalle intercambiable sin cambiar el resto del contrato. +- **`FileAccess.open()` fallido devuelve `null`, no un `false`**: el retorno `bool` de `store_var` solo cubre fallos con un handle ya abierto; una apertura fallida (disco lleno, permisos, `user://` no escribible) devuelve `null` y llamar `.store_var()` sobre él crashea. *Mitigación*: chequeo obligatorio de `FileAccess.get_open_error()` inmediatamente tras cada `open()`, antes de cualquier `store_var`/`get_var` (ver invariantes) — un modo de fallo distinto del que cubre el retorno `bool`, ambos enrutados al camino de reintento/`CORRUPTED`, nunca a un crash. +- **Deserialización de Objetos como superficie de ataque**: si un sistema entregara una instancia `RefCounted`/`Resource` viva a `store_var` (o alguien activara `allow_objects=true` al cargar), un save manipulado podría instanciar Objetos arbitrarios. *Mitigación*: invariante loud-fail — `serialize_state()` solo devuelve `Variant` planos; `full_objects`/`allow_objects` quedan en su default `false`; grep CI sobre `store_var(..., true)`/`get_var(..., true)`. El diseño ya lo evita (cada sistema entrega un `Dictionary` plano), pero se hace regla explícita en vez de detalle implícito del diagrama. +- **Determinismo de `hash()` entre versiones de Godot**: si el proyecto migrara de motor en el futuro, un guardado viejo podría fallar su checksum simplemente por el cambio de versión, no por corrupción real. *Mitigación*: fuera de alcance de este ADR — una migración de motor sería un evento manual, auditado, y `CURRENT_SCHEMA_VERSION` ya provee el mecanismo para tratar guardados de una build distinta explícitamente (aunque hoy el eje de versión es de contenido/esquema, no de motor). Registrado como riesgo aceptado, no bloqueante. +- **Disciplina de `remaining_ticks` no enforced automáticamente**: nada impide hoy que un futuro sistema serialice un `expiry_tick` absoluto por error. *Mitigación*: grep de CI sobre el símbolo `game_tick`/`expiry_tick` fuera de los módulos de `TimeControl`/rebasing dedicados (mismo patrón de enforcement que ADR-0002 usa para `_process(delta)`), documentado en el control-manifest. +- **`store_var`/`store_buffer` retorno `bool` no verificado en cada punto de escritura**: si un programador olvida chequear el retorno en un paso intermedio, un fallo de disco a mitad de escritura podría pasar desapercibido hasta el rename. *Mitigación*: `request_save()` centraliza todos los pasos de escritura en un solo método de `SaveService` — no hay puntos de escritura dispersos por el código de otros sistemas que puedan omitir el chequeo. + +## GDD Requirements Addressed + +| GDD System | Requirement | How This ADR Addresses It | +|------------|-------------|--------------------------| +| `guardado-persistencia.md` | Regla 5 (escritura atómica `save.tmp`→rename); AC-12/13/14/24/25/26/27 | `request_save()` centraliza la escritura, usa el retorno `bool` de `store_var`/`store_buffer` (post-4.4) para abortar antes del rename en fallo de escritura temporal; el rename es la única operación que puede producir `CORRUPTED` | +| `guardado-persistencia.md` | Regla 6 (versionado de esquema, migración lineal v→v+1); AC-15/16/17/18 | Migraciones como funciones puras sobre `Dictionary` crudo, aplicadas antes de `deserialize_state`; `schema_version > CURRENT` bloquea sin migrar (`VERSION_TOO_NEW`) | +| `guardado-persistencia.md` | Regla 7 (checksum sin auto-reparación); AC-19/20/21/30; Open Question de algoritmo | `hash()` de Godot sobre el payload; recalculado y comparado al cargar; mismatch → `CORRUPTED`, ninguna escritura de reparación (resuelve el Open Question del GDD) | +| `forja-de-legado.md` | OQ-FL4 punto 3 (fidelidad `int64` de `forge_counter`); AC-FL11 | `store_var`/`get_var` preserva `int` de 64 bits nativamente, sin codificación especial — `forge_counter` nunca pasa por un `double` | +| `reliquias-bendiciones.md` | OQ-RB2 / AC-RB46 (round-trip de `invocation_charges_remaining` y modificadores activos con expiración restante) | Contrato `serialize_state()` de Reliquias persiste `charges_remaining` + `remaining_ticks` por modificador (no `expiry_tick` absoluto); rebasing al cargar contra `TimeControl.game_tick` fresco (0) | +| ADR-0001 (`combate-dano.md`/`reliquias-bendiciones.md`) | Persistencia diferida del estado efímero de invocación y de `StatModifier`, incl. rebasing de `expiry_tick` | Resuelto vía `remaining_ticks` relativo — `game_tick` nunca se persiste; `StatModifier.expiry_tick` se reconstruye al cargar | +| ADR-0002 (`permadeath.md`/`reliquias-bendiciones.md`/`temporizador...md`) | Rebasing de `expiry_tick` definido contra `TimeControl.game_tick`, no un contador de motor | `remaining_ticks` se calcula contra `game_tick` en el momento de guardar; `game_tick` en sí permanece puramente por sesión, sin cambios a su diseño | +| `sistema-de-heroes.md` / `permadeath.md` | AC-H26/AC-H27c (payload de muerte pendiente de sellar, recuperación por crash mid-beat) | Sub-diccionario opaco con campo explícito `sealed: bool`; el flujo de carga invoca la misma coacción `DYING→DEAD` que un crash en vivo | + +## Performance Implications +- **CPU**: `store_var`/`get_var` sobre un payload de decenas de campos (referencias/IDs/contadores, nunca copias de `EraDefinition` — Regla 4 del GDD) es sub-milisegundo, consistente con la nota de diseño ya presente en el GDD (Formulas). El cálculo de `hash()` es O(tamaño del payload), despreciable al mismo orden de magnitud. +- **Memory**: Sin estructuras persistentes adicionales en memoria — el payload se ensambla y se libera por guardado/carga. +- **Load Time**: Nulo más allá de la lectura de un único archivo pequeño; las migraciones (cuando existan) son transformaciones de `Dictionary` en memoria, sin I/O adicional. +- **Network**: N/A (sin multijugador en el MVP). + +## Migration Plan +No hay código aún (fase de diseño). En implementación: (1) crear `SaveService` (Core, inyectable, con `FakeFileAccess`/ruta temporal para tests). (2) Cada sistema dependiente (Forja, Reliquias, Temporizador, Datos de Era, Héroes, Transición de Era) implementa `serialize_state()`/`deserialize_state()` y se registra vía `register_persistable`. (3) Reliquias adapta su estado de invocación activo para computar `remaining_ticks` en `serialize_state()` en vez de exponer `expiry_tick` absoluto. (4) Héroes incluye el campo `sealed` en su payload de muerte pendiente. (5) `CURRENT_SCHEMA_VERSION` arranca en 1; la primera migración real se escribe cuando el primer cambio de esquema lo requiera (no antes — no hay migraciones especulativas). **Ediciones cross-GDD a aplicar al aceptar**: `reliquias-bendiciones.md` (OQ-RB2 cerrado, contrato real de round-trip reemplaza el mock `save_ephemeral_invocation_state`/`load_ephemeral_invocation_state`), `forja-de-legado.md` (OQ-FL4 punto 3 confirmado — `store_var`/`get_var` es la API elegida), `guardado-persistencia.md` (Open Question de algoritmo de checksum resuelto → `hash()`). + +## Validation Criteria +- Test unitario: `request_save()`/`request_load()` round-trip de un payload con un `int` de valor `> 2^53` (p. ej. `forge_counter` simulado) preserva el valor exacto — prueba directa de que `store_var` no trunca donde JSON lo haría. +- Test unitario: checksum recalculado sobre un payload sin modificar coincide; sobre un payload con un byte alterado, no coincide → `CORRUPTED`. +- Test unitario: un modificador activo con `remaining_ticks = 300` persistido, cargado con `TimeControl.game_tick` reiniciado en 0, produce `expiry_tick == 300` tras la carga (no arrastra el `game_tick` de la sesión anterior). +- Test unitario: `request_save()` cuyo paso de escritura a `save.tmp` retorna `false` (mock de fallo de disco) aborta antes del rename; el archivo de guardado final no cambia. +- Test de integración: una cadena de 2 migraciones simuladas (`v1→v2→v3`) se aplica en orden estricto sobre un `Dictionary` con `schema_version=1`, nunca salta a `v3` directamente. +- Test de integración: payload de muerte pendiente con `sealed=false` al cargar dispara la coacción `DYING→DEAD` (mock del hook de Permadeath/Héroes). +- Loud-fail: `schema_version` del guardado mayor que `CURRENT_SCHEMA_VERSION` → bloqueo explícito, cero migraciones ejecutadas, cero intento de "continuar de todos modos". +- CI: grep del símbolo `game_tick`/`expiry_tick` fuera de `time_control.gd` y del código de rebasing de guardado dedicado → cero apariciones sin justificar. + +## Related Decisions +- ADR-0001 (capa de modificadores de stat por instancia) — este ADR resuelve el hand-off de persistencia que ADR-0001 dejó explícitamente abierto. +- ADR-0002 (servicio `TimeControl`) — este ADR resuelve el rebasing de `expiry_tick` contra `game_tick` que ADR-0002 dejó explícitamente abierto, sin modificar el diseño de `TimeControl`. +- Futuro ADR: representación de `RelicRecord` (Forja de Legado, OQ-FL4 — Resource `.tres` vs dato serializado) puede apoyarse en la elección de `store_var`/`get_var` de este ADR. +- `docs/architecture/architecture-review-2026-08-21.md` — bloqueante **S4** (origen de este ADR). +- `design/gdd/guardado-persistencia.md`, `design/gdd/forja-de-legado.md`, `design/gdd/reliquias-bendiciones.md`, `design/gdd/sistema-de-heroes.md`, `design/gdd/permadeath.md` — GDDs a reconciliar al aceptar. diff --git a/docs/architecture/adr-0004-era-lifecycle-init-ordering.md b/docs/architecture/adr-0004-era-lifecycle-init-ordering.md new file mode 100644 index 0000000000..987ecc7670 --- /dev/null +++ b/docs/architecture/adr-0004-era-lifecycle-init-ordering.md @@ -0,0 +1,264 @@ +# ADR-0004: Ciclo de vida de era y orden de inicialización — servicio `EraManager` + +## Status +Proposed + +## Date +2026-08-21 + +## Engine Compatibility + +| Field | Value | +|-------|-------| +| **Engine** | Godot 4.6 | +| **Domain** | Core (carga de recursos, orden de inicialización de Autoloads, orquestación de arranque) | +| **Knowledge Risk** | MEDIUM | +| **References Consulted** | `docs/engine-reference/godot/VERSION.md`; `docs/engine-reference/godot/breaking-changes.md` (4.5: `duplicate_deep()` para Resources anidados — **no** usado aquí, el `EraDefinition` es read-only); `docs/engine-reference/godot/deprecated-apis.md` (sin entradas de I/O de recursos o carga) | +| **Post-Cutoff APIs Used** | Ninguna. La decisión usa `ResourceLoader.load()` **síncrono** (estable desde 4.0) y el orden de init de Autoloads (estable). La API threaded (`load_threaded_request`/`get_status`/`get`) queda como camino de migración explícito, **no** usada en el MVP. | +| **Verification Required** | (1) Confirmar en 4.6 el orden de init de Autoloads: se inicializan en el orden listado en `project.godot`, **antes** del árbol de la escena principal — la regla de init-ordering depende de esto (confirmado por godot-specialist 2026-08-21: estable, sin cambios hasta 4.6). (2) Confirmar que `ResourceLoader.load(path)` síncrono con `CACHE_MODE_REUSE` (default) devuelve la misma instancia compartida del `.tres` de era en cargas repetidas (confirmado; coherente con "inmutable, compartido por era"). (3) Una referencia de sub-recurso `@export` rota (`kaiju_definition`/roster ausente) dentro de un `.tres` que **sí** parsea deja ese campo en **`null`** y Godot imprime un mensaje a consola **no atrapable** desde GDScript — **no** hay un error de retorno/excepción que inspeccionar. El único señal observable programáticamente es el campo `null`, así que el gate lo atrapa con un **check explícito de resolución de referencias** (no esperando un valor de retorno de `ResourceLoader.load`). Distinto del recurso top-level ausente, donde `ResourceLoader.load` sí devuelve `null` (ver Decision). | + +## ADR Dependencies + +| Field | Value | +|-------|-------| +| **Depends On** | None (duro). Interactúa en el arranque con ADR-0002 (`TimeControl`, Autoload) y ADR-0003 (`SaveService`, Autoload): el orden de los tres Autoloads y la secuencia de boot se fijan aquí, pero ninguno es prerequisito de Accepted del otro. | +| **Enables** | Epics de todo sistema que lee datos de era en el momento correcto (Tropas, Héroes, Kaiju, Temporizador, Permadeath, Reliquias, Forja); el ADR futuro de Transición de Era (#15) hereda esta máquina de estados; el ADR de Cámara/Control puede reubicar `world_unit_scale`/`camera_pan_speed`/`zoom_*` sin tocar este contrato | +| **Blocks** | Ningún epic puede implementar lectura de datos de era hasta que este ADR sea Accepted (define cuándo/cómo es seguro leer una era y quién conduce el ciclo de vida) | +| **Ordering Note** | Cuarto ADR. Segundo gap Foundation del `/architecture-review 2026-08-21` (tras Save/Persistencia S4). No bloquea a ADR-0003 ni al revés, pero la secuencia de boot (SaveService → EraManager → era_loaded → consumidores) los ata en implementación. | + +## Context + +### Problem Statement + +`datos-de-era-civilizacion.md` (Foundation, el sistema del que más otros dependen) ya define la máquina de estados de una era — `UNLOADED → LOADING → LOADED → ACTIVE → ARCHIVED` (States and Transitions, AC-7..AC-12/AC-15/AC-19) — y ya establece contratos clave que la asumen: +- **Permadeath** lee `get_hero_roster_count()` **sobre la señal era-`LOADED`, explícitamente no en su propio `_ready()`** (Datos de Era Interactions/Dependencies, cierra Permadeath OQ-P6 #6) — para validar la feasibilidad de su cap de bloqueo total (Regla 8). +- El GDD exige **loud-fail al cargar `EraDefinition`** para varias validaciones cross-resource: `max_band_size ≥ Σ starting_count_[tipo]` (R1, 2026-08-21, Tropas AC-34), y "en el mismo camino de carga centralizado que los checks cross-resource de Kaiju (AC-K74/K85)". El Temporizador valida `approach_duration_s > 0` y `0 < imminent_window_s < approach_duration_s` al inicializarse (AC-T17/T18). Permadeath valida su cap contra `get_hero_roster_count()`. +- Edge Case: "si un sistema solicita `unit_roster` mientras la era está en `LOADING`, la solicitud se bloquea/encola hasta `LOADED`; nunca datos parciales" (AC-15). + +Pero **nada de esto tiene un dueño ni un mecanismo fijado**: +- **Nadie conduce la máquina de estados.** El `EraDefinition` es un `Resource` puro de datos (Regla 1/3: inmutable, sin lógica) — no puede poseer su propio ciclo de carga, validación, ni emitir señales. No hay un sistema que ejecute `UNLOADED→LOADING→LOADED→ACTIVE→ARCHIVED`. +- **El mecanismo de carga async (`LOADING→LOADED`) no está fijado** — ni síncrono ni threaded. +- **El contrato de la señal era-`LOADED` no está formalizado.** Permadeath lo asume, pero no hay firma, dueño, ni una regla general de que *todos* los consumidores lean en esa señal. +- **La regla de init-ordering no está escrita** — y es la trampa central: en Godot, el `_ready()` de un Autoload corre al arranque del motor, **antes** de que ninguna era esté cargada. Un sistema que lea datos de era en su `_ready()` (roster, kaiju, `max_band_size`, `get_hero_roster_count`, `invocation_charges`, `approach_duration_s`…) leería `null`/basura. Este es exactamente el bug que el contrato de Permadeath evita puntualmente, pero sin una regla general todos los demás consumidores pueden caer en él. +- **Las validaciones cross-resource loud-fail están dispersas** entre GDDs (Datos de Era, Kaiju, Temporizador, Permadeath, Tropas) sin un gate único que las ejecute en un solo punto antes de que ningún consumidor lea. + +El `/architecture-review 2026-08-21` marcó esto como el segundo gap Foundation (`TR-era-011`, `TR-permadeath-017`). + +### Constraints + +- **`EraDefinition` es un `Resource` puro, inmutable, sin lógica** (Datos de Era Reglas 1/3/5) — el ciclo de vida NO puede vivir dentro del Resource; necesita un servicio separado. +- **Loud-fail sobre degradación silenciosa** (precedente Héroes/Combate/ADR-0001/0002/0003) — una era con datos inválidos (config cross-resource rota, referencia ausente) debe fallar ruidosamente al cargar, nunca activarse parcialmente ni sustituir defaults (Datos de Era AC-16). +- **DI sobre singletons** (coding-standards) — testeable en aislamiento; el ciclo de vida no debe acoplarse a Autoloads por nombre global; los tests inyectan una era falsa / una ruta de test. +- **Exactamente una era `ACTIVE` a la vez** (Datos de Era AC-9), y `ARCHIVED→ACTIVE` prohibido sin un ciclo `LOADING→LOADED` fresco (AC-11/AC-12). +- **Interacción de arranque con dos Autoloads ya decididos**: `SaveService` (ADR-0003) determina **qué** `era_id` cargar (el save persiste el `era_id` activo por referencia, nunca copia `EraDefinition`); `TimeControl` (ADR-0002) es independiente de la era pero comparte el espacio de orden de Autoloads. La secuencia de boot debe quedar definida. +- **MVP de una sola era**: el invariante de "una `ACTIVE`" y las transiciones deben probarse aunque el MVP tenga `narrative_order=0` sin siguiente era (Datos de Era AC-14). + +### Requirements + +- Un dueño único del ciclo de vida de era que conduzca `UNLOADED→LOADING→LOADED→ACTIVE→ARCHIVED`. +- Un mecanismo de carga fijado para `LOADING→LOADED`. +- Una señal `era_loaded` con firma y contrato definidos, y una **regla de init-ordering** explícita: los sistemas leen datos de era en `era_loaded`, nunca en `_ready()`. +- Un **gate de validación centralizado** que ejecute todas las validaciones cross-resource loud-fail en un solo punto, antes de que ningún consumidor lea. +- Una secuencia de boot definida entre `SaveService` → `EraManager` → consumidores. +- Testeable: inyectable, sin RNG ni I/O real en la lógica de estados (mock de carga / era falsa). + +## Decision + +**Se introduce un servicio Core `EraManager` que posee y conduce el ciclo de vida de era.** El `EraDefinition` permanece un `Resource` de datos puro e inmutable; `EraManager` es el único que lo carga, valida, transiciona entre estados y anuncia esos cambios por señal. Ningún otro sistema conduce el ciclo ni instancia eras directamente. + +**Carga síncrona (`ResourceLoader.load()`), `LOADING→LOADED` instantáneo en un frame.** `EraManager.load_era(era_id)` transiciona `UNLOADED→LOADING`, llama `ResourceLoader.load(path)` (bloqueante, `CACHE_MODE_REUSE` default → instancia compartida del `.tres`), y **antes de nada chequea si el retorno es `null`**: un recurso top-level ausente/no-parseable (ej. `era_id` inexistente de un save manipulado o corrupto, o un typo de ruta en dev) hace que `ResourceLoader.load()` devuelva `null` — el flujo va directo a `FAILED` con la razón `"era_resource_not_found"`, **sin** invocar el gate (llamar `_validate(null)` o acceder a cualquier campo de una era nula crashearía, rompiendo el invariante "nunca crashea, siempre `FAILED`"). Este caso es distinto de una **referencia de sub-recurso rota dentro** de un `.tres` que sí parsea (ej. `kaiju_definition` no resuelve): ahí `ResourceLoader.load()` devuelve un `EraDefinition` **no-nulo** con el campo roto en `null`, y Godot imprime un error a consola (no una excepción atrapable); ese caso lo atrapa el check explícito de resolución de referencias del gate. Solo si el retorno es no-nulo se ejecuta el gate y se transiciona a `LOADED`. Para el MVP de una era con `.tres` pequeños esto es determinista y sin polling; el Edge Case "encolar solicitudes durante `LOADING`" (AC-15) se satisface trivialmente porque la ventana `LOADING` es una sola llamada síncrona. **La máquina de estados se diseña para admitir carga threaded después sin cambiar el contrato**: si en producción aparecen hitches o se quiere una pantalla de carga entre eras, `LOADING` pasa a ser un estado multi-frame conducido por `ResourceLoader.load_threaded_request`/`get_status`/`get`, y `era_loaded` sigue emitiéndose igual al completar — los consumidores no cambian. + +**Gate de validación centralizado en `LOADING→LOADED`, antes de `era_loaded`.** `EraManager` ejecuta **todas** las validaciones cross-resource loud-fail en una sola pasada al terminar la carga, antes de emitir `era_loaded`: +- `max_band_size ≥ Σ starting_count_[tipo]` sobre `unit_roster` (Datos de Era R1 / Tropas AC-34). +- Feasibilidad de la garantía de ≥30s de Kaiju: `ceil(hero_max_health / kaiju_attack_damage) × kaiju_attack_cooldown_s + telegraph_duration_s ≥ 30` (Kaiju F2 / AC-K74) y los checks cross-resource de AC-K85. +- Feasibilidad del cap de bloqueo total de Permadeath: `death_hold_total_lockout_cap_s ≥ get_hero_roster_count() × death_hold_min_compressed_s` (Permadeath Regla 8). +- Rangos del Temporizador: `approach_duration_s > 0`, `0 < imminent_window_s < approach_duration_s` (Temporizador AC-T17/T18). +- Resolución de referencias: `kaiju_definition` y las referencias del `unit_roster` cargan (no rotas/ausentes) — Datos de Era AC-16. + +**Semántica de fallo — hard stop, nunca activación parcial.** Si cualquier validación falla, `era_loaded` **NO se emite**, la era entra a un estado terminal **`FAILED`** (extensión de la máquina de estados del GDD para el camino de error), se emite `era_load_failed(era_id, razones)`, y la era **nunca alcanza `ACTIVE`**. El control de flujo (no emitir `era_loaded`, llegar a `FAILED`, emitir `era_load_failed`) es **build-agnóstico** — se comporta igual en dev y en release, no depende de visibilidad de build. Se acompaña de `push_error()` (siempre se ejecuta, dev y release) para dejar traza; un `assert()` adicional es un trampa **solo de dev, suplementaria, nunca load-bearing** para el control de flujo (se compila a no-op en export de release — misma distinción que ADR-0002 dibuja para sus asserts defensivos). Un `EraDefinition` con config cross-resource rota es un error de autoría que jamás debe llegar a jugarse; el gate lo atrapa en un punto, no diez sistemas a medias. (Cada GDD dueño de un check retiene la **especificación** del invariante; `EraManager` es el punto único de **ejecución** — no reimplementa la lógica de dominio, invoca a cada validador.) + +**Señal `era_loaded(era: EraDefinition)` + regla de init-ordering.** Los consumidores se conectan a `era_loaded` y leen los datos de era **en el handler de esa señal**, recibiendo el `EraDefinition` por referencia. La regla dura: **ningún sistema lee datos de era en su `_ready()` / en la inicialización de Autoload** — el `_ready()` corre al arranque del motor, antes de que exista ninguna era `LOADED`, así que una lectura ahí devuelve `null`/basura. Esto generaliza el contrato puntual que Permadeath ya tiene (leer `get_hero_roster_count()` en era-`LOADED`, no en `_ready()`) a **todos** los consumidores. Se registra como patrón prohibido (grep CI: lecturas de campos de `EraDefinition` en `_ready()`). + +**Secuencia de boot (orquestación entre Autoloads).** Los tres Autoloads Core (`EraManager`, `TimeControl`, `SaveService`) en su `_ready()` **solo se cablean** — ninguno lee datos de era. Un composition root (la escena principal / un orquestador de arranque) conduce: +1. `SaveService.request_load()` → determina el `era_id` activo (o `NO_SAVE_EXISTS` → partida nueva → era con `narrative_order = 0`). +2. `EraManager.load_era(era_id)` → `UNLOADED→LOADING` (`ResourceLoader.load`) → gate de validación → `LOADED` (emite `era_loaded(era)`) **o** `FAILED` (loud-fail). +3. Los consumidores conectados a `era_loaded` leen sus porciones; el estado persistido **por era** (cargas de invocación, modificadores activos con `remaining_ticks` — ADR-0003) se aplica en este punto. +4. `EraManager.activate_era()` → `LOADED→ACTIVE` (emite `era_activated(era)`). + +`TimeControl` es independiente de la era (su `game_tick` arranca en 0 por sesión — ADR-0002/0003); no participa de esta secuencia salvo por compartir el orden de Autoloads. + +**Restricción de posición del composition root (evita la carrera de señal perdida — godot-specialist, revisión 2026-08-21).** Como la carga es síncrona, `era_loaded` se emite **dentro del mismo call stack** de `load_era()` — no hay ventana multi-frame, y las señales de Godot **no se re-emiten** para suscriptores tardíos. Que "todos los consumidores estén conectados antes de que `load_era()` corra" **no** lo garantiza el orden Autoload-vs-escena por sí solo; depende de dónde viva el composition root en el árbol. El `_ready()` intra-escena de Godot es **bottom-up** (hijos antes que el padre), así que la garantía se sostiene si y solo si **el composition root es ancestro de todos los consumidores de `era_loaded` y dispara `load_era()` desde su propio `_ready()` (o más tarde)** — para entonces los `_ready()` de todos los consumidores descendientes ya corrieron y ya conectaron. Esta es una restricción de posición en el árbol, no solo de orden de Autoloads; se documenta en el control-manifest. + +**Nodos instanciados post-boot (tropa spawneada a mitad de partida, panel de UI perezoso).** Por definición `era_loaded` ya se emitió cuando existen, así que **no** pueden depender de atraparla — leen el estado de era llamando **`EraManager.get_active_era()` directamente** (seguro una vez hay una era `ACTIVE`). Corolario para el enforcement: el grep de CI "sin lecturas de `EraDefinition` en `_ready()`" debe **exceptuar explícitamente** las lecturas vía `get_active_era()` en `_ready()` cuando ya hay una era `ACTIVE` — es un patrón legítimo para nodos dinámicos post-boot; la regla prohíbe leer campos de era en `_ready()` **asumiendo que ya hay era cargada al boot**, no leer vía el accesor cuando genuinamente ya la hay. + +**Wiring: Autoload sostenido pero inyectado por referencia** (mismo patrón que `TimeControl` de ADR-0002 y `SaveService` de ADR-0003). Un ciclo de vida de era canónico es un concern transversal legítimo; la inyección preserva la testabilidad DI-sobre-singletons. Prohibido `get_node("/root/EraManager")` disperso: los consumidores reciben la referencia en una pasada de wiring (composition root) o se conectan a `era_loaded` vía esa referencia. + +### Architecture Diagram + +``` + Boot (composition root / escena principal): + SaveService.request_load() ──► era_id activo (o narrative_order=0 en partida nueva) + │ + ▼ + ┌──────────────────────────────────────────────────────────────┐ + │ EraManager (Core Autoload, inyectado por referencia) │ + │ │ + │ load_era(era_id): │ + │ UNLOADED → LOADING (ResourceLoader.load, síncrono) │ + │ │ │ + │ ▼ gate de validación (una pasada, loud-fail): │ + │ · max_band_size ≥ Σ starting_count (Tropas AC-34) │ + │ · Kaiju ≥30s feasibility (Kaiju AC-K74/K85) │ + │ · Permadeath cap ≥ roster×min (Permadeath R8) │ + │ · Temporizador approach/imminent (AC-T17/T18) │ + │ · refs kaiju/roster resuelven (Datos AC-16) │ + │ │ │ + │ ├─ pasa → LOADED ──emit era_loaded(era)──┐ │ + │ └─ falla → FAILED (push_error, NUNCA ACTIVE) │ + │ │ │ + │ activate_era(): LOADED → ACTIVE ─emit era_activated(era) │ + │ archive_active_era(): ACTIVE → ARCHIVED ─emit era_archived │ + └────────────────────────────────────────────────┼─────────────┘ + │ era_loaded(era) + Consumidores (leen en el handler, NUNCA en _ready()): + Tropas(unit_roster,max_band_size) · Héroes(roster) · Kaiju(kaiju_definition) + · Temporizador(approach/imminent) · Permadeath(get_hero_roster_count) + · Reliquias(invocation_charges) · Forja(origin_era_id de la era ACTIVE) + · SaveService (aplica estado persistido por-era) + + REGLA DE INIT-ORDERING: el _ready() de un Autoload corre al boot, ANTES de + que ninguna era esté LOADED. Leer datos de era en _ready() = null/basura. + → Todo consumidor lee en era_loaded, nunca en _ready(). (patrón prohibido, grep CI) +``` + +### Key Interfaces + +```gdscript +# EraManager — servicio Core. Autoload que sostiene el ciclo de vida, PERO inyectado por +# referencia (no EraManager.* por nombre disperso). Los tests inyectan un cargador falso / +# un EraDefinition de test para probar la máquina de estados sin ResourceLoader ni disco. +class_name EraManager extends Node + +enum EraState { UNLOADED, LOADING, LOADED, ACTIVE, ARCHIVED, FAILED } + +signal era_loaded(era: EraDefinition) # SOLO tras pasar el gate de validación +signal era_activated(era: EraDefinition) +signal era_archived(era: EraDefinition) +signal era_load_failed(era_id: StringName, reasons: PackedStringArray) # compañero loud-fail + +var current_state: EraState = EraState.UNLOADED +var active_era: EraDefinition = null # null hasta que una era esté ACTIVE + +func load_era(era_id: StringName) -> void +# UNLOADED->LOADING: var era := ResourceLoader.load(_path_for(era_id)) (síncrono, CACHE_MODE_REUSE). +# if era == null: -> FAILED + push_error + emit era_load_failed(era_id, ["era_resource_not_found"]) +# (recurso top-level ausente/corrupto; NO se llama _validate, evita crash null-ref) +# Solo si era != null: corre _validate(era); si pasa -> LOADED + emit era_loaded(era); +# si devuelve violaciones -> FAILED + push_error + emit era_load_failed(era_id, violaciones). +# NUNCA llega a LOADED/ACTIVE con una era nula o con validaciones rotas. +func activate_era() -> void # LOADED -> ACTIVE (exactamente una ACTIVE); emit era_activated +func archive_active_era() -> void # ACTIVE -> ARCHIVED; emit era_archived; active_era = null +func get_active_era() -> EraDefinition # null si ninguna ACTIVE + +# Gate de validación centralizado (loud-fail, corre antes de era_loaded). +# EraManager EJECUTA; cada GDD dueño retiene la ESPECIFICACIÓN del invariante. +func _validate(era: EraDefinition) -> PackedStringArray # devuelve violaciones; vacío = ok +# - max_band_size >= sum(starting_count_[tipo]) (Tropas AC-34 / Datos R1) +# - ceil(hero_max_health/kaiju_attack_damage)*attack_cooldown_s +# + telegraph_duration_s >= 30 (Kaiju F2/AC-K74) +# - checks cross-resource de Kaiju AC-K85 +# - death_hold_total_lockout_cap_s +# >= era.get_hero_roster_count() * death_hold_min_compressed_s (Permadeath R8) +# - approach_duration_s > 0 ; 0 < imminent_window_s < approach_duration_s (Temporizador AC-T17/18) +# - kaiju_definition y refs de unit_roster resuelven (no rotas) (Datos AC-16) +``` + +**Contrato de invariantes (loud-fail):** +- `era_loaded` se emite **solo** tras pasar el gate completo — nunca con una violación pendiente. +- Exactamente una era en `ACTIVE` en todo momento (Datos de Era AC-9). `activate_era()` desde un estado ≠ `LOADED` es un error. +- `ARCHIVED→ACTIVE` sin un ciclo `LOADING→LOADED` fresco está prohibido (AC-11/AC-12). +- Ningún sistema lee campos de `EraDefinition` en su `_ready()` / init de Autoload **asumiendo que ya hay era cargada al boot** — los consumidores de boot leen en el handler de `era_loaded`. Excepción legítima (no viola la regla): un nodo instanciado **post-boot** puede leer vía `EraManager.get_active_era()` en su `_ready()` cuando ya hay una era `ACTIVE`. El grep de CI exceptúa las lecturas vía `get_active_era()`. +- `FAILED` es terminal para ese intento de carga; se sale de él solo con un nuevo `load_era` (nunca se "repara" una era inválida en sitio). +- `EraManager` **nunca** muta el `EraDefinition` (Resource inmutable, Datos de Era R3/AC-3). + +## Alternatives Considered + +### Alternative 1: Plegar el ciclo de vida en `SaveService` (ADR-0003) +- **Description**: `SaveService` ya determina qué `era_id` cargar; extenderlo para también cargar el `EraDefinition`, validarlo y emitir `era_loaded`. +- **Pros**: Un Autoload menos; la secuencia "qué era → cargar era" queda en un solo lugar. +- **Cons**: Mezcla dos concerns distintos — `SaveService` es infraestructura de *serialización de estado mutable* (ADR-0003), agnóstica de dominio; el ciclo de vida de era es *orquestación de contenido* con un gate de validación cross-resource específico del dominio. Acoplarlos hace a `SaveService` conocer la validación de Kaiju/Permadeath/Tropas, rompiendo su neutralidad de dominio. Además Transición de Era (#15) conducirá `ACTIVE→ARCHIVED→siguiente` — eso es lógica de era, no de guardado. +- **Rejection Reason**: Viola la separación de concerns que ADR-0003 fijó explícitamente (`SaveService` no conoce el significado de los campos). El ciclo de vida es un dueño Core propio. + +### Alternative 2: Sin dueño — cada consumidor carga y valida su porción +- **Description**: Cada sistema carga el `EraDefinition` (o su porción) cuando lo necesita y valida su propio slice en `era_loaded`. +- **Pros**: Sin servicio nuevo; cada sistema "posee" lo que le importa. +- **Cons**: Es el estado roto actual — sin dueño de la máquina de estados, sin punto único que garantice "una `ACTIVE`", validaciones loud-fail dispersas que dejan que una era rota active parcialmente antes de que un consumidor la atrape, y N cargas del mismo Resource. La regla de init-ordering no tendría un emisor canónico de `era_loaded`. +- **Rejection Reason**: Exactamente lo que el review marcó como gap; no da atomicidad de "válida o nada" ni un dueño de las transiciones. + +### Alternative 3: Carga threaded (`load_threaded_*`) desde el MVP +- **Description**: Usar `ResourceLoader.load_threaded_request`/`get_status`/`get` para una carga async real en hilo de fondo desde el inicio. +- **Pros**: `LOADING` es un estado multi-frame genuino; permite pantalla de carga y escala a eras grandes. +- **Cons**: Añade polling de estado, gestión de hilo y una superficie de estados más compleja que el MVP de una sola era con `.tres` pequeños no necesita. La rearquitectura de interpolación de física 3D de 4.5 no aplica aquí, pero la carga threaded tiene sus propias aristas (orden de completado, errores async) que no vale la pena pagar hoy. +- **Rejection Reason**: Sobre-ingeniería para el MVP. La máquina de estados se diseña para admitirlo después sin cambiar el contrato de `era_loaded` — migración local cuando (si) haga falta. Registrado como evolución en Consequences → Risks. + +## Consequences + +### Positive +- Cierra el segundo gap Foundation del review: el ciclo de vida de era tiene un dueño, un mecanismo de carga, un contrato de señal, una regla de init-ordering y un gate de validación único. +- La regla de init-ordering elimina una clase entera de bugs de arranque (leer era en `_ready()` → `null`) para *todos* los consumidores, no solo Permadeath. +- El gate centralizado da atomicidad "era válida o ninguna": una config cross-resource rota (autoría) falla en un punto, nunca activa a medias. +- `EraDefinition` sigue siendo un Resource puro (Datos de Era R1/R3 intactos); la lógica vive en `EraManager`, no en el dato. +- Testeable en aislamiento: la máquina de estados y el gate se prueban inyectando un `EraDefinition` de test y un cargador falso, sin disco ni Autoload por nombre. +- Da a Transición de Era (#15) una máquina de estados y un dueño ya definidos sobre los que construir `ACTIVE→ARCHIVED→siguiente`. + +### Negative +- Sistema Core nuevo (`EraManager`) que no estaba numerado en el systems-index — añadir como servicio Core de soporte (igual que `TimeControl` y `SaveService`). +- Disciplina requerida: cada consumidor debe conectarse a `era_loaded` y **no** leer en `_ready()` — enforced por revisión/CI, no por el compilador. +- `EraManager` centraliza la *ejecución* de validaciones cuya *especificación* vive en 4-5 GDDs distintos; hay que mantener el gate sincronizado si un GDD dueño cambia su invariante (mitigado: el gate invoca a cada validador, no reimplementa la lógica). + +### Risks +- **Deriva entre el gate y las specs de los GDDs**: si Kaiju/Permadeath/Tropas cambian un invariante y el gate no se actualiza, una era rota podría pasar. *Mitigación*: el gate llama a un validador por dominio (idealmente un método del propio Resource/sistema dueño), no copia la fórmula; test de CI que cruza los invariantes documentados con los checks del gate. +- **Evolución a carga threaded**: si aparecen hitches de carga o se quiere pantalla de carga entre eras. *Mitigación*: la máquina de estados ya contempla `LOADING` como estado nombrado; el cambio es local a `load_era` y no toca `era_loaded` ni a los consumidores. +- **Orden de Autoloads mal configurado**: si `EraManager`/`SaveService`/`TimeControl` se listan en un orden que rompa el boot. *Mitigación*: ninguno lee era en `_ready()` (solo se cablean), así que el orden de _init_ de Autoloads no importa para corrección; la secuencia real la conduce el composition root explícitamente, no el orden de la lista. Documentar en el control-manifest. +- **`CACHE_MODE_REUSE` y mutación accidental**: como el `.tres` se comparte por `CACHE_MODE_REUSE`, una mutación accidental afectaría a todas las referencias. *Mitigación*: `EraDefinition` es read-only por contrato (Datos de Era R3/AC-3); assert/`push_error` en debug ante escritura, documentado en control-manifest (mismo patrón que `CombatProfile` en ADR-0001). **Nota (godot-specialist):** un ciclo "fresco" `ARCHIVED→LOADING→LOADED` (AC-11/12) re-corre el gate de validación contra la **misma instancia en memoria** cacheada, **no** re-lee bytes del disco — "fresco" significa "re-entra al ciclo de estados y re-valida", no "re-cargado desde disco". Es el comportamiento deseado dado que `EraDefinition` es inmutable; la re-validación es barata y no puede divergir del disco porque nada mutó la instancia. + +## GDD Requirements Addressed + +| GDD System | Requirement | How This ADR Addresses It | +|------------|-------------|--------------------------| +| `datos-de-era-civilizacion.md` | Máquina de estados `UNLOADED→LOADING→LOADED→ACTIVE→ARCHIVED` (AC-7..12); "una `ACTIVE`" (AC-9); `ARCHIVED→ACTIVE` prohibido sin recarga (AC-11/12); encolar durante `LOADING` (AC-15); loud-fail de refs rotas (AC-16) | `EraManager` conduce la máquina de estados con esos invariantes; carga síncrona hace la ventana `LOADING` una llamada (AC-15 trivial); refs rotas → `FAILED` (AC-16) | +| `datos-de-era-civilizacion.md` | `max_band_size ≥ Σ starting_count_[tipo]` loud-fail al cargar (R1, 2026-08-21) | Parte del gate de validación centralizado, antes de `era_loaded` | +| `permadeath.md` | Lee `get_hero_roster_count()` sobre era-`LOADED`, **no** en `_ready()`; feasibilidad del cap Regla 8 | La regla de init-ordering generaliza este contrato; la feasibilidad del cap es un check del gate (`TR-permadeath-017`) | +| `encuentro-con-kaiju.md` | Garantía ≥30s loud-fail al cargar `EraDefinition` (F2/AC-K74) + cross-resource AC-K85 | Checks del gate centralizado, ejecutados por `EraManager`, spec retenida por Kaiju | +| `sistema-de-tropas.md` | `max_band_size` enforcement (Fórmula E); validación AC-34 | Check del gate; `unit_roster`/`max_band_size` leídos por Tropas en `era_loaded`, no en `_ready()` | +| `temporizador-de-preparacion-ritual.md` | `approach_duration_s > 0`, `0 < imminent_window_s < approach_duration_s` (AC-T17/T18) | Checks del gate centralizado | +| `reliquias-bendiciones.md` / `forja-de-legado.md` | Reliquias lee `invocation_charges` por era (AC-RB17); Forja etiqueta `origin_era_id` de la era `ACTIVE` (AC-FL01/FL26) | Ambos leen en `era_loaded`/consultan `get_active_era()`; `origin_era_id` = `era_id` de la era `ACTIVE` | + +## Performance Implications +- **CPU**: `ResourceLoader.load` síncrono de un `.tres` pequeño + una pasada de validación O(nº checks) por carga de era — sub-milisegundo a unos ms, una vez por transición de era (raro). Despreciable. +- **Memory**: Un `EraDefinition` compartido en memoria por era `LOADED`/`ACTIVE` (`CACHE_MODE_REUSE`), más el servicio. Trivial. +- **Load Time**: La carga síncrona bloquea un frame en la transición de era; aceptable para el MVP (una era). Si crece, migrar a threaded (ver Risks). +- **Network**: N/A. + +## Migration Plan +No hay código aún. En implementación: (1) crear `EraManager` (Core Autoload + interfaz inyectable + cargador falso de test). (2) Definir la secuencia de boot en el composition root: `SaveService.request_load()` → `EraManager.load_era(era_id)` → `era_loaded` → consumidores → `activate_era()`. (3) Cada consumidor (Tropas, Héroes, Kaiju, Temporizador, Permadeath, Reliquias, Forja) conecta a `era_loaded` y **mueve toda lectura de datos de era fuera de `_ready()`**. (4) Consolidar las validaciones cross-resource dispersas en `_validate()`, invocando el validador de cada dominio. **Ediciones cross-GDD a aplicar al aceptar**: `datos-de-era-civilizacion.md` (nombrar a `EraManager` como conductor del ciclo de vida y dueño del gate; añadir el estado `FAILED` a la tabla de States; nota de init-ordering), nota en `permadeath.md`/`sistema-de-tropas.md`/`encuentro-con-kaiju.md`/`temporizador-de-preparacion-ritual.md` (leen en `era_loaded`, validación ejecutada por el gate). Añadir `EraManager` al systems-index como servicio Core de soporte. + +## Validation Criteria +- Test unitario: `load_era` con un `EraDefinition` de test válido → `UNLOADED→LOADING→LOADED`, emite `era_loaded` exactamente una vez. +- Test unitario: `load_era` con un `era_id` inexistente (el cargador falso devuelve `null`) → `FAILED` con razón `"era_resource_not_found"`, `era_loaded` **no** se emite, **sin** crash null-ref (no se invoca el gate). +- Test unitario: `load_era` con `max_band_size < Σ starting_count` → `FAILED`, `era_loaded` **no** se emite, `era_load_failed` sí, con la razón correcta. +- Test unitario: nodo instanciado post-boot (tras `era_activated`) que llama `get_active_era()` en su `_ready()` → obtiene la era `ACTIVE`; no depende de `era_loaded` (ya emitida). +- Test unitario: gate atrapa cada violación (Kaiju ≥30s, cap de Permadeath, rangos del Temporizador, ref rota) — un caso por check. +- Test unitario: `activate_era()` desde `LOADED` → `ACTIVE`, exactamente una `ACTIVE`; desde un estado ≠ `LOADED` → error. +- Test unitario: `ARCHIVED→ACTIVE` directo → rechazado; requiere `LOADING→LOADED` primero (AC-11/12). +- Test de integración: un consumidor que lee `unit_roster` en `era_loaded` obtiene el roster completo; el mismo consumidor no lee nada en `_ready()` (grep CI). +- CI: grep de lecturas de campos de `EraDefinition` en `_ready()` / init de Autoload → cero. +- Loud-fail: era con config cross-resource inválida nunca alcanza `ACTIVE`. + +## Related Decisions +- ADR-0002 (`TimeControl`) — Autoload Core hermano; independiente de la era pero comparte el orden de Autoloads y el patrón inyectado-por-referencia. +- ADR-0003 (`SaveService`) — determina qué `era_id` cargar; la secuencia de boot los ata (SaveService → EraManager → era_loaded → aplicar estado persistido por-era). +- Futuro ADR: Transición de Era (#15) hereda esta máquina de estados para `ACTIVE→ARCHIVED→siguiente`. +- Futuro ADR: Cámara/Control — puede reubicar `world_unit_scale`/`camera_pan_speed`/`zoom_*` (hoy en Datos de Era por necesidad) sin tocar este contrato. +- `docs/architecture/architecture-review-2026-08-21.md` — gap Foundation Era lifecycle (`TR-era-011`, `TR-permadeath-017`). +- GDDs a reconciliar al aceptar: `datos-de-era-civilizacion.md`, `permadeath.md`, `sistema-de-tropas.md`, `encuentro-con-kaiju.md`, `temporizador-de-preparacion-ritual.md`. diff --git a/docs/architecture/architecture-review-2026-08-21.md b/docs/architecture/architecture-review-2026-08-21.md new file mode 100644 index 0000000000..215bc132fd --- /dev/null +++ b/docs/architecture/architecture-review-2026-08-21.md @@ -0,0 +1,231 @@ +# Architecture Review Report + +**Date:** 2026-08-21 +**Engine:** Godot 4.6 (pinned 2026-02-12) +**GDDs Reviewed:** 13 system GDDs + concept + systems-index +**ADRs Reviewed:** 2 (ADR-0001, ADR-0002 — both `Proposed`) +**Mode:** full +**Verdict:** **CONCERNS** + +> Loaded 13 GDDs, 2 ADRs, engine Godot 4.6. TR registry was empty before this run +> (first review) — 399 TR-IDs newly assigned. Architecture registry: 2 state_ownership, +> 2 interfaces, 0 performance_budgets, 0 api_decisions, 4 forbidden_patterns. +> `docs/architecture/architecture.md` does not exist yet. + +--- + +## Traceability Summary + +| Metric | Count | +|--------|-------| +| Total technical requirements extracted | **399** | +| ✅ Covered by an accepted-scope ADR | ~19 | +| ⚠️ Partial | ~8 | +| ❌ Gap (no ADR) | ~372 | +| **Coverage** | **~5%** | + +Per-system TR counts: input 29 · era 20 · save 24 · control 25 · tropas 25 · heroes 33 · +combate 38 · kaiju 44 · temporizador 19 · permadeath 32 · forja 29 · reliquias 41 · ui 40. + +The ~5% figure is **not** a defect signal. It is what a two-ADR architecture looks like +against a 13-system corpus. The meaningful question is not "why are 372 uncovered" — most +await ADRs that were never meant to exist at this stage — but **"do the GDDs self-declare +architecture decisions that remain unwritten?"** The answer is yes: roughly **seven** +beyond the two that exist. + +--- + +## What the Two ADRs Cover (and cover well) + +**ADR-0001 — instance combat-stat modifier layer.** Lands the Pillar-2 damage hook: +`effective_attack_damage()`, `CombatState`, flat-additive per-instance modifiers, +refresh-on-repick dedup, expiry-before-phase-1, "expiry never kills." Covers +`TR-reliquias-003/004/005/006/007/009/021/022` and honours the Combate single-damage-path +constraint. This is exactly **F2-B1** from the 2026-08-18 cross-review. + +**ADR-0002 — `TimeControl` service.** Lands time-regime authority: `PAUSED > SLOWED > +NORMAL` precedence, `game_time_paused` with identical prior semantics, `game_tick` +pause/scale-aware integer clock superseding the provisional `Engine.get_physics_frames()`. +Covers `TR-permadeath-003/004`, `TR-reliquias-024/025/026`, `TR-combate-029`, +`TR-kaiju-029`, `TR-temporizador-010`. This is exactly **F2-B4/S2**. + +The cross-review named two architecture blockers; both now have sound ADRs. + +--- + +## Cross-ADR Conflict Detection + +**No blocking conflicts.** The two ADRs own disjoint state (`instance_combat_stats` vs +`game_time_regime`) and the architecture registry records both without contradiction. + +- **Near-cycle, resolved by stated ordering.** ADR-0002 `Depends On: ADR-0001`, while + ADR-0001's expiry sweep reads ADR-0002's `game_tick`. Not a true cycle: ADR-0001's core + (modifier layer + effective-stat computation) needs no clock; only the *sweep timing* + consumes `game_tick`. Clean order: **ADR-0001 structure → ADR-0002 clock → wire expiry.** +- **Minor internal inconsistency (not a cross-ADR conflict).** ADR-0001 Verification + Required item 1 still asserts in present tense that `Engine.get_physics_frames()` "es la + llamada correcta"; ADR-0002 supersedes it. Fix the stale text at Accepted-time (the + Risks / Related-Decisions sections already frame it correctly). + +### Recommended ADR Implementation Order (topologically sorted) + +``` +Foundation (no ADR dependencies): + 1. ADR-0001 Instance combat-stat modifier layer +Depends on Foundation: + 2. ADR-0002 TimeControl (requires ADR-0001) +``` + +Both must reach `Accepted` before any dependent epic — stories referencing a `Proposed` +ADR are auto-blocked, and ADR-0002 cannot be safely implemented until ADR-0001 is +`Accepted`. + +--- + +## Engine Compatibility (Phase 5 + godot-specialist consultation) + +**Verdict: both ADRs are engine-safe to move to `Accepted`. No blocking engine issue.** + +Audit results: + +- Engine consistency: both declare Engine Compatibility, both agree Godot 4.6 — no drift. +- Deprecated API references: **zero** across both ADRs. +- Post-cutoff APIs: both declare "Ninguna"; ADR-0002 actively *rejects* `Engine.time_scale` + and `SceneTree.paused`. +- Shared invariant: both assert `Engine.physics_ticks_per_second == 60` at startup. +- The one real post-cutoff change in scope (4.5 3D-interpolation rearchitecture) is + correctly ruled 3D-only, irrelevant to this 2D MVP. +- Engine Compatibility section coverage: 2 / 2 ADRs. + +### Engine Specialist Findings + +- **[MEDIUM · ADR-0002]** `TimeControl` per-frame ordering vs. consumers is assumed, not + enforced. "Sweep before phase-1 damage" needs `TimeControl._physics_process` to advance + `game_tick` before Combat reads it; today that holds only by Autoload registration + artifact. **Fix:** explicit `process_physics_priority = -1000` + a CI/test assertion. + A stale read is a silent off-by-one expiry — contradicts the project's loud-fail standard. +- **[LOW · ADR-0002]** Actually set `PROCESS_MODE_ALWAYS` on `TimeControl` (make the + documented defense real). +- **[LOW · ADR-0001]** Reconcile the stale `Engine.get_physics_frames()` present-tense text + in Verification Required item 1. +- **[LOW · both]** Document the `StringName` (`source_id`) vs `String` (`source_relic_id`) + type-choice rationale so the asymmetry reads as intentional. +- **[LOW · ADR-0001]** Note the `CombatProfile` default-cache-mode (`CACHE_MODE_REUSE`) + assumption that underpins "shared by type." +- **[LOW · ADR-0002]** Add a slow-mo-smoothness validation criterion (continuous scaled + delta, never frame-skip) — ADVISORY per the Visual/Feel test-evidence tier. + +--- + +## GDD Revision Flags (Architecture → Design feedback) + +One flag, **low severity**: + +| GDD | Assumption | Reality (ADR-0002) | Action | +|-----|-----------|--------------------|--------| +| sistema-de-heroes.md (V-5/V-6 note), permadeath.md (Regla 4 corollary) | `PROCESS_MODE_ALWAYS` is *required* for the death-beat clock/animation to survive pause | ADR-0002: the real exemption is "exempt systems never poll TimeControl — they read real-time delta directly"; `PROCESS_MODE_ALWAYS` is defensive, **not** load-bearing (nothing sets `SceneTree.paused`) | Reconcile the *why* (not the *what* — marking it ALWAYS is harmless) | + +**Resolution (user decision, 2026-08-21):** fold into ADR-0002's acceptance reconciliation +(permadeath.md is already on ADR-0002's Migration Plan list). **No systems-index status +change** — this is a *why-not-what* clarification and nothing breaks. + +--- + +## Coverage Gaps That Matter — Required ADRs + +The GDDs self-declare **~7 architecture decisions the two existing ADRs do not cover.** +Prioritized, Foundation first. + +### Foundation layer (highest priority) + +- ❌ **Save / Persistence ADR** — checksum algorithm (Save GDD: "a resolver en un ADR"), + serialization format, ephemeral-invocation-state round-trip (`OQ-RB2`), **`expiry_tick` + rebasing across reload**, `forge_counter` int64 via `store_var`/`get_var`, schema + migration chain. This is cross-review blocker **S4**, and *both existing ADRs explicitly + defer to it.* Engine Risk: MEDIUM. Suggested: `/architecture-decision save-persistence` +- ❌ **Era lifecycle / init-ordering ADR** — `LOADING→LOADED` async load, era-`LOADED` + signal contract, Autoloads reading roster on era-`LOADED` not `_ready()` (`TR-era-011`, + `TR-permadeath-017`), per-era cap re-validation. Engine Risk: MEDIUM. +- ❌ **Camera/zoom constant-ownership ADR** — `world_unit_scale` / `camera_pan_speed` / + `zoom_min` / `zoom_max` (Datos de Era vs Control), `Camera2D.zoom` sign mapping + (`OQ-INPUT-2`). Resolves cross-review **F2-W5**. Small but unblocks a live dispute. +- ❌ **Input context-enforcement ADR** — `current_context` get-only + + `request_context_transition()` as cross-cutting contract (`OQ-INPUT-5`), plus + frame-ordering of the distributed Pillar-1 invariant Input→Control→Permadeath + (`OQ-INPUT-1`). + +### Feature layer + +- ❌ **`CombatProfile` storage ADR** — Combate `OQ-1` (embedded in `TroopDefinition` / + `HeroDefinition` vs parallel referenced resource). The combate GDD marks it "requiere ADR + antes de implementar"; **ADR-0001 explicitly left it open.** +- ❌ **Relic-registry representation ADR** — Forja `OQ-FL4/FL5` + Reliquias `OQ-RB6`: + `RelicRecord` runtime home (Autoload vs node), serialization, same-frame no-deferred + deposit, `relic_id` encoding, seeded-RNG injection. A coordinated pair. +- ❌ **Kaiju intent-controller ADR** — the GDD's "Aún para el ADR": where the intent FSM + lives, immediate (non-`CONNECT_DEFERRED`) `apply_damage` wiring, and the hard requirement + that the kaiju is the *sole* caller of `apply_damage`, excluded from Combate's generic + auto-attack loop. +- ❌ **Permadeath implementation-pattern ADR** — `OQ-P5`: Autoload vs node vs event-bus, + `reset_queue()` ownership + the input-suppression softlock class, `animation_done` / + `force_relic_reveal_final()` hook forms (`OQ-P2`), and the 4.6 engine spike + (`PROCESS_MODE_ALWAYS` + unscaled delta + `Engine.time_scale` independence — the pinned + reference docs do **not** cover these, so the spike the GDD asks for is genuinely needed). + +### Cross-cutting convention (ADR or control-manifest) + +- ❌ **Config-enforcement + determinism convention** — the **loud-fail-on-load vs + clamp-on-load split** is currently *undecided across ~10 GDDs* (Héroes/Combate/Permadeath/ + Kaiju/Reliquias/Forja = loud-fail; Tropas/Save/Control = clamp). Kaiju specifies a precise + 4.6 mechanism (custom setter + `push_error()` + halt — **not** `assert()` alone, + `@export_range`, or `_validate_property`). Plus the recurring seeded-RNG / + monotonic-`entity_id`-not-`get_instance_id()` determinism seam (Kaiju AC-K71, Reliquias + AC-RB18, Forja Regla 4). No ADR ratifies either; both belong in the control-manifest at + minimum. + +--- + +## Architecture Document Coverage (Phase 6) + +`docs/architecture/architecture.md` **does not exist** — no master blueprint to validate +systems against yet. Expected: `/create-architecture` has not run, and the cross-review +says not to run it until the blockers clear. Recorded as the reason a full system↔layer +coverage check cannot be performed at this stage. + +--- + +## Pre-Gate Checklist (Phase 9) + +| Artifact | Status | Action | +|----------|--------|--------| +| `tests/unit/`, `tests/integration/` | ❌ | run `/test-setup` | +| `.github/workflows/tests.yml` | ❌ | run `/test-setup` | +| `design/ux/interaction-patterns.md` | ❌ | run `/ux-design` | +| `design/accessibility-requirements.md` / `design/ux/accessibility-requirements.md` | ❌ | run `/ux-design` | + +`/gate-check pre-production` is **not** offerable — test infrastructure and UX/accessibility +artifacts are absent, on top of the ADR gaps above. + +--- + +## Verdict: CONCERNS + +- The two ADRs that exist are **sound, mutually consistent, engine-safe, and correctly + target the two architecture blockers the cross-review named.** After the minor cleanups + (stale `get_physics_frames()` text; the MEDIUM ordering guard), **both are ready to move + `Proposed → Accepted`.** +- But the architecture as a body is **early and incomplete**: the GDDs self-declare **~7 + further ADR-level decisions**, several Foundation-layer (Save/persistence most urgently — + both existing ADRs defer to it). **This cannot pass the Pre-Production gate yet.** + +Not FAIL: no blocking cross-ADR conflict, and the existing decisions are not broken. +Not PASS: named Foundation-layer requirements sit uncovered. + +### Immediate actions (highest-impact first) + +1. Apply the two ADR cleanups, then move ADR-0001 + ADR-0002 to `Accepted` (in that order). +2. Write the **Save/Persistence ADR** — Foundation *and* the explicit dependency of both + existing ADRs. +3. Write the **`CombatProfile` storage ADR** (Combate OQ-1) and the **Era init-ordering ADR**. + +Re-run `/architecture-review` after each new ADR to verify coverage improves. diff --git a/docs/architecture/tr-registry.yaml b/docs/architecture/tr-registry.yaml index 92cab0e2eb..632ce794cb 100644 --- a/docs/architecture/tr-registry.yaml +++ b/docs/architecture/tr-registry.yaml @@ -19,38 +19,434 @@ # /story-readiness (validate TR-ID exists and is active) # # ID FORMAT: TR-[system-slug]-[NNN] -# system-slug = short slug matching the GDD system name -# NNN = three-digit zero-padded sequence per system, starting at 001 -# # STATUS VALUES: active | deprecated | superseded-by: TR-[system]-NNN -version: 1 -last_updated: "" +version: 2 +last_updated: "2026-08-21" # First real population: 399 TR-IDs from /architecture-review 2026-08-21 -requirements: [] +requirements: -# --- EXAMPLE ENTRIES (remove when first real entries are added) --- -# -# - id: TR-combat-001 -# system: combat -# gdd: design/gdd/combat-system.md -# requirement: "Player takes damage from enemies based on attack power minus defence" -# created: 2026-03-10 -# revised: "" -# status: active -# -# - id: TR-combat-002 -# system: combat -# gdd: design/gdd/combat-system.md -# requirement: "Combo window timing is 0.4 seconds between hits" -# created: 2026-03-10 -# revised: "2026-04-01" -# status: active -# -# - id: TR-combat-003 -# system: combat -# gdd: design/gdd/combat-system.md -# requirement: "Old damage formula using raw stats" -# created: 2026-03-10 -# revised: "" -# status: superseded-by: TR-combat-001 + # ─── INPUT (Foundation) — design/gdd/input.md ─────────────────────────────── + - { id: TR-input-001, system: input, gdd: design/gdd/input.md, domain: Platform, anchor: "Regla Nucleo 1 / AC 1", requirement: "Every physical device maps to a fixed set of named Input Actions (StringName const in one autoload); consumers never read raw devices.", created: 2026-08-21, revised: "", status: active } + - { id: TR-input-002, system: input, gdd: design/gdd/input.md, domain: Engine, anchor: "Regla Nucleo 1", requirement: "Input must connect to Input.joy_connection_changed(device, connected) — the only discrete device connect/disconnect signal (explicit exception to no-raw-devices).", created: 2026-08-21, revised: "", status: active } + - { id: TR-input-003, system: input, gdd: design/gdd/input.md, domain: Data, anchor: "Regla Nucleo 1", requirement: "Active gamepad tracked by stable identity (GUID / Input.get_joy_name), never by device index — SDL3 backend (4.5+) reassigns indices on reconnect.", created: 2026-08-21, revised: "", status: active } + - { id: TR-input-004, system: input, gdd: design/gdd/input.md, domain: Ownership, anchor: "Regla Nucleo 2 / AC 3", requirement: "Exactly one active input context at a time (RTS_COMMAND/MENU_NAVIGATION/BLESSING_SELECT/DEATH_HOLD) as a single enum field _current_context, not booleans, managed by an in-Input state machine.", created: 2026-08-21, revised: "", status: active } + - { id: TR-input-005, system: input, gdd: design/gdd/input.md, domain: Communication, anchor: "Regla Nucleo 2", requirement: "Context gating is centralized: _input handlers consult current_context and never emit an intent signal for a disabled action; gating is not delegated to consumers.", created: 2026-08-21, revised: "", status: active } + - { id: TR-input-006, system: input, gdd: design/gdd/input.md, domain: Ownership, anchor: "Regla Nucleo 2 / AC 4", requirement: "current_context is get-only over private _current_context; transitions only via request_context_transition(new)->bool (GDScript 4.6 has no field privacy — pattern-enforced).", created: 2026-08-21, revised: "", status: active } + - { id: TR-input-007, system: input, gdd: design/gdd/input.md, domain: Ownership, anchor: "States and Transitions", requirement: "Only Input changes context, triggered by other systems' events (Permadeath forces DEATH_HOLD, Reliquias requests BLESSING_SELECT), never by the player directly.", created: 2026-08-21, revised: "", status: active } + - { id: TR-input-008, system: input, gdd: design/gdd/input.md, domain: Communication, anchor: "Regla Nucleo 3 / AC 5", requirement: "Input exposes active_input_method and emits input_method_changed same-frame; scope is prompt-icon choice only, not focus-ring visibility.", created: 2026-08-21, revised: "", status: active } + - { id: TR-input-009, system: input, gdd: design/gdd/input.md, domain: Ownership, anchor: "Edge Case / AC 5c", requirement: "Method switch requires a deliberate press (pressed action or sustained stick deflection >= min frame threshold, debounce/hysteresis); the threshold is an Input-owned contract consumers must not reimplement.", created: 2026-08-21, revised: "", status: active } + - { id: TR-input-010, system: input, gdd: design/gdd/input.md, domain: Platform, anchor: "AC 5b", requirement: "Before any physical input, active_input_method returns a platform-detected default (KEYBOARD_MOUSE on PC, GAMEPAD on console), never uninitialized.", created: 2026-08-21, revised: "", status: active } + - { id: TR-input-011, system: input, gdd: design/gdd/input.md, domain: Communication, anchor: "Regla Nucleo 4 / AC 6 / AC 21", requirement: "Input only emits intent signals with raw position data (select_requested, box_select_requested, move_commanded, pan_requested, zoom_requested, blessing_choice_confirmed, blessing_selection_cancelled, unit_selected_at, input_device_changed) — no unit lookup, no state mutation.", created: 2026-08-21, revised: "", status: active } + - { id: TR-input-012, system: input, gdd: design/gdd/input.md, domain: Timing, anchor: "AC 14/15/16", requirement: "Transition to DEATH_HOLD is immediate (same frame) and discards any queued/in-flight input; no order is buffered or replayed on release.", created: 2026-08-21, revised: "", status: active } + - { id: TR-input-013, system: input, gdd: design/gdd/input.md, domain: Communication, anchor: "Visual/Audio / AC 15b", requirement: "Input exposes input_lockout_active=true and hides the mouse cursor for the whole DEATH_HOLD; on release returns to false and cursor reappears per target context — mandatory feedback contract, not cosmetic.", created: 2026-08-21, revised: "", status: active } + - { id: TR-input-014, system: input, gdd: design/gdd/input.md, domain: Timing, anchor: "AC 17b (contract w/ permadeath Regla 4 / AC-P07c)", requirement: "On gamepad disconnect during DEATH_HOLD, Input emits reconnect prompt but does NOT trigger a real engine pause (SceneTree.paused stays false); the pause request is deferred until Permadeath releases the beat.", created: 2026-08-21, revised: "", status: active } + - { id: TR-input-015, system: input, gdd: design/gdd/input.md, domain: Platform, anchor: "AC 17", requirement: "On gamepad disconnect outside DEATH_HOLD, system keeps active context and emits input_device_changed(keyboard_mouse) + pause request + reconnect prompt within one frame.", created: 2026-08-21, revised: "", status: active } + - { id: TR-input-016, system: input, gdd: design/gdd/input.md, domain: Performance, anchor: "AC 22", requirement: "Input latency: every intent signal emitted within one frame of the OS event (<=1 frame / <=16.6ms), measured over 60s with 0 dropped or 2+-frame-delayed events on target hardware.", created: 2026-08-21, revised: "", status: active } + - { id: TR-input-017, system: input, gdd: design/gdd/input.md, domain: Performance, anchor: "AC 22 (measurement)", requirement: "The measurement harness records event timestamp at _input callback and signal-emission timestamp, counting delta-frame>1 and coalesced/dropped events; coalescence policy must be defined (coalesce mouse_motion OK; dropping a pressed not OK).", created: 2026-08-21, revised: "", status: active } + - { id: TR-input-018, system: input, gdd: design/gdd/input.md, domain: Performance, anchor: "AC 22 / OQ-INPUT-4", requirement: "No 'Input' row exists in the studio frame-time budget table; requires an explicit ms allocation from the 16.6ms budget and a place where latency is tracked.", created: 2026-08-21, revised: "", status: active } + - { id: TR-input-019, system: input, gdd: design/gdd/input.md, domain: Data, anchor: "Formula 1", requirement: "Radial/circular dead zone with remap: stick_input_mag=(raw_mag-stick_dead_zone)/(1-stick_dead_zone), stick_input_dir=Vector2(x,y)/raw_mag; inclusive <=; mag in [0,1] guaranteed.", created: 2026-08-21, revised: "", status: active } + - { id: TR-input-020, system: input, gdd: design/gdd/input.md, domain: Communication, anchor: "Formula 1 (source of truth)", requirement: "stick_input_dir and stick_input_mag are mandatory exported symbols: control-y-seleccion Formula 3 consumes them directly.", created: 2026-08-21, revised: "", status: active } + - { id: TR-input-021, system: input, gdd: design/gdd/input.md, domain: Platform, anchor: "Formula 2 (DPI scaling)", requirement: "drag_threshold_px_effective = drag_threshold_px * (viewport_height_px/1080.0), derived at runtime; the safe-range clamp (3-8) applies to the reference value, not the effective one.", created: 2026-08-21, revised: "", status: active } + - { id: TR-input-022, system: input, gdd: design/gdd/input.md, domain: Engine, anchor: "Formula 3", requirement: "Double-click detection uses a manual timer comparing Time.get_ticks_msec() between InputEventMouseButton events, NOT OS-level double-click (inconsistent cross-platform).", created: 2026-08-21, revised: "", status: active } + - { id: TR-input-023, system: input, gdd: design/gdd/input.md, domain: Data, anchor: "Formula 5 / AC 13/13a/13b", requirement: "Zoom scalar computed with two separate expressions (zoom_in: zoom*(1-f); zoom_out: zoom*(1+f)) with zoom_min=0.5=zoomed-in, zoom_max=1.5=zoomed-out; invert_zoom swaps which physical input fires each, never the scalar.", created: 2026-08-21, revised: "", status: active } + - { id: TR-input-024, system: input, gdd: design/gdd/input.md, domain: Ownership, anchor: "Formula 5 / OQ-INPUT-2", requirement: "Input defines the design zoom scalar; translation to Camera2D.zoom (Vector2) is owned by Control y Seleccion — explicit ownership split.", created: 2026-08-21, revised: "", status: active } + - { id: TR-input-025, system: input, gdd: design/gdd/input.md, domain: Data, anchor: "AC 24b", requirement: "A zoom value outside [zoom_min, zoom_max] from a config change is clamped on the next input frame; out-of-bounds zoom is never allowed.", created: 2026-08-21, revised: "", status: active } + - { id: TR-input-026, system: input, gdd: design/gdd/input.md, domain: Ownership, anchor: "Tuning Knobs (accessibility)", requirement: "Knobs stick_dead_zone, double_click_window_s, drag_threshold_px, invert_zoom are single source of truth feeding Accessibility by reference (no duplication); Accessibility may need larger ceilings than standard tuning ranges.", created: 2026-08-21, revised: "", status: active } + - { id: TR-input-027, system: input, gdd: design/gdd/input.md, domain: Platform, anchor: "UI Requirements / AC 2", requirement: "A remapping screen must read/write the InputMap; Input must expose the named actions readably for that screen.", created: 2026-08-21, revised: "", status: active } + - { id: TR-input-028, system: input, gdd: design/gdd/input.md, domain: Ownership, anchor: "Player Fantasy / OQ-INPUT-1", requirement: "The Pillar-1 invariant is cross-system with named owners: Input owns raw-signal fidelity; Control owns click->unit resolution; Permadeath owns lethality/sealing — requires an explicit handoff contract among the three.", created: 2026-08-21, revised: "", status: active } + - { id: TR-input-029, system: input, gdd: design/gdd/input.md, domain: Timing, anchor: "OQ-INPUT-1 (b)", requirement: "Frame ordering among Input (last-second rescue order), Control (apply it), Permadeath (evaluate the death) must be specified so an update-order accident is never indistinguishable from a legitimate player decision.", created: 2026-08-21, revised: "", status: active } + + # ─── DATOS DE ERA/CIVILIZACION (Foundation) — design/gdd/datos-de-era-civilizacion.md ─── + - { id: TR-era-001, system: era, gdd: design/gdd/datos-de-era-civilizacion.md, domain: Data, anchor: "Regla 1 / AC-1", requirement: "Each era is a typed Godot Resource (EraDefinition, .tres) with era_id, era_name, civilization_name, narrative_order, unit_roster, kaiju_definition, tileset_vocabulary, color_accent, legacy_props, world_unit_scale, approach_duration_s, imminent_window_s, all correctly typed.", created: 2026-08-21, revised: "", status: active } + - { id: TR-era-002, system: era, gdd: design/gdd/datos-de-era-civilizacion.md, domain: Data, anchor: "Regla 1 / AC-1b", requirement: "EraDefinition exposes derived accessor get_hero_roster_count()->int, recomputed from unit_roster on each call (not a cacheable-desync field, not a separate authored field).", created: 2026-08-21, revised: "", status: active } + - { id: TR-era-003, system: era, gdd: design/gdd/datos-de-era-civilizacion.md, domain: Ownership, anchor: "Regla 3 / AC-3", requirement: "Era definitions are runtime-immutable: any write to an ACTIVE EraDefinition field is rejected or does not persist; re-read returns the original authored value.", created: 2026-08-21, revised: "", status: active } + - { id: TR-era-004, system: era, gdd: design/gdd/datos-de-era-civilizacion.md, domain: Data, anchor: "Regla 2 / AC-2", requirement: "100% authored, no procedural generation: no era/kaiju/unit data generated at runtime — verifiable by static grep of generation calls in consumers.", created: 2026-08-21, revised: "", status: active } + - { id: TR-era-005, system: era, gdd: design/gdd/datos-de-era-civilizacion.md, domain: Data, anchor: "Regla 5 / AC-6", requirement: "Data-driven, never hardcoded: no consumer may have era/kaiju/unit names in code; all resolved via EraDefinition references.", created: 2026-08-21, revised: "", status: active } + - { id: TR-era-006, system: era, gdd: design/gdd/datos-de-era-civilizacion.md, domain: Data, anchor: "Regla 4 / AC-5", requirement: "legacy_props stores relic IDs (string/enum), never an embedded relic structure — Forja de Legado remains the single source of truth.", created: 2026-08-21, revised: "", status: active } + - { id: TR-era-007, system: era, gdd: design/gdd/datos-de-era-civilizacion.md, domain: Data, anchor: "States and Transitions / AC-7..12", requirement: "Per-era lifecycle state machine UNLOADED->LOADING->LOADED->ACTIVE->ARCHIVED, with reload ARCHIVED->LOADING->LOADED; direct ARCHIVED->ACTIVE is rejected.", created: 2026-08-21, revised: "", status: active } + - { id: TR-era-008, system: era, gdd: design/gdd/datos-de-era-civilizacion.md, domain: Ownership, anchor: "AC-9", requirement: "Invariant: exactly one EraDefinition is ACTIVE at a time (must be proven even with a single MVP era).", created: 2026-08-21, revised: "", status: active } + - { id: TR-era-009, system: era, gdd: design/gdd/datos-de-era-civilizacion.md, domain: Timing, anchor: "Edge Case LOADING / AC-15", requirement: "If a system requests unit_roster while the era is LOADING, the request blocks/queues until LOADED; partial data is never returned — implies async load with wait.", created: 2026-08-21, revised: "", status: active } + - { id: TR-era-010, system: era, gdd: design/gdd/datos-de-era-civilizacion.md, domain: Engine, anchor: "Edge Case / AC-16", requirement: "A broken/missing kaiju_definition reference must fail loudly at load/editor time (visible error/assert); never silently substitute a default kaiju.", created: 2026-08-21, revised: "", status: active } + - { id: TR-era-011, system: era, gdd: design/gdd/datos-de-era-civilizacion.md, domain: Timing, anchor: "Interactions/Dependencies Permadeath (2026-08-16)", requirement: "Permadeath reads get_hero_roster_count() on the era-LOADED signal, not in its own _ready() — requires an observable era lifecycle signal and an init-order contract.", created: 2026-08-21, revised: "", status: active } + - { id: TR-era-012, system: era, gdd: design/gdd/datos-de-era-civilizacion.md, domain: Ownership, anchor: "Formulas world_unit_scale", requirement: "This system is the registered source of truth of world_unit_scale=48 px/unit, consumed by Input and Control y Seleccion.", created: 2026-08-21, revised: "", status: active } + - { id: TR-era-013, system: era, gdd: design/gdd/datos-de-era-civilizacion.md, domain: Ownership, anchor: "Formulas camera_pan_speed/zoom", requirement: "This system also owns camera_pan_speed=30, zoom_min=0.5, zoom_max=1.5 as registered constants; consumers apply but never redefine them. (Ownership contested — see camera/zoom-ownership ADR gap.)", created: 2026-08-21, revised: "", status: active } + - { id: TR-era-014, system: era, gdd: design/gdd/datos-de-era-civilizacion.md, domain: Ownership, anchor: "Regla 1 (range validation)", requirement: "approach_duration_s and imminent_window_s are guaranteed only in existence and type (float); range validation (loud-fail) is owned by the Temporizador at init.", created: 2026-08-21, revised: "", status: active } + - { id: TR-era-015, system: era, gdd: design/gdd/datos-de-era-civilizacion.md, domain: Performance, anchor: "Edge Case ARCHIVED / AC-19", requirement: "An ARCHIVED era may unload tileset_vocabulary for performance; if later queried it reloads to LOADED (not ACTIVE) on demand — implies explicit asset load/unload management.", created: 2026-08-21, revised: "", status: active } + - { id: TR-era-016, system: era, gdd: design/gdd/datos-de-era-civilizacion.md, domain: Data, anchor: "Tuning Knobs / AC-20", requirement: "max_legacy_props_visible is clamped to safe range [4,20] before use, and the real legacy_props list is never truncated by this limit (only the rendered subset).", created: 2026-08-21, revised: "", status: active } + - { id: TR-era-017, system: era, gdd: design/gdd/datos-de-era-civilizacion.md, domain: Engine, anchor: "Edge Case color_accent / AC-18", requirement: "Undefined color_accent uses a neutral fallback in release builds but a visible placeholder color in development builds — build-type-dependent behavior.", created: 2026-08-21, revised: "", status: active } + - { id: TR-era-018, system: era, gdd: design/gdd/datos-de-era-civilizacion.md, domain: Persistence, anchor: "Regla 3 / AC-4", requirement: "Guardado stores only era_id (reference), and the EraDefinition .tres must remain byte-for-byte identical to its authored version after any save.", created: 2026-08-21, revised: "", status: active } + - { id: TR-era-019, system: era, gdd: design/gdd/datos-de-era-civilizacion.md, domain: Data, anchor: "Edge Case repeated ID / AC-17", requirement: "A single relic ID may appear in legacy_props of multiple eras and must be present in all their queries — legacy accumulates, is not consumed.", created: 2026-08-21, revised: "", status: active } + - { id: TR-era-020, system: era, gdd: design/gdd/datos-de-era-civilizacion.md, domain: Data, anchor: "Edge Case single era / AC-14", requirement: "With a single era (narrative_order=0), the 'next era' query finds none and does not activate, without throwing — the expected MVP state.", created: 2026-08-21, revised: "", status: active } + + # ─── GUARDADO/PERSISTENCIA (Foundation) — design/gdd/guardado-persistencia.md ─── + - { id: TR-save-001, system: save, gdd: design/gdd/guardado-persistencia.md, domain: Ownership, anchor: "Overview / AC-01", requirement: "System exposes a read/write API with no game logic; consumers trigger saves — implies a stable public interface testable via simulated triggers.", created: 2026-08-21, revised: "", status: active } + - { id: TR-save-002, system: save, gdd: design/gdd/guardado-persistencia.md, domain: Timing, anchor: "Regla 1 / AC-04", requirement: "Consequence-event autosaves are blocking: the write must complete before the game advances to the next screen/state; transition held until SAVE_EXISTS or a failure path is surfaced.", created: 2026-08-21, revised: "", status: active } + - { id: TR-save-003, system: save, gdd: design/gdd/guardado-persistencia.md, domain: Ownership, anchor: "Regla 1 / AC-01b", requirement: "Mandatory non-configurable autosave triggers: hero transition to DYING (fired by Sistema de Heroes AC-H26, not Permadeath), relic forge (Forja), end of era (Transicion de Era).", created: 2026-08-21, revised: "", status: active } + - { id: TR-save-004, system: save, gdd: design/gdd/guardado-persistencia.md, domain: Persistence, anchor: "Interactions Heroes", requirement: "Persisted death snapshot must include the full death payload (hero_id, hero_name, relic_category, relic_quality already computed, D/T/P) marked as 'death pending sealing' — not just state=DYING.", created: 2026-08-21, revised: "", status: active } + - { id: TR-save-005, system: save, gdd: design/gdd/guardado-persistencia.md, domain: Persistence, anchor: "AC-01b (contract AC-H27c)", requirement: "Load flow must be able to coerce DYING->DEAD and forge the relic if the game crashed mid-beat — recovery logic on the load path.", created: 2026-08-21, revised: "", status: active } + - { id: TR-save-006, system: save, gdd: design/gdd/guardado-persistencia.md, domain: Persistence, anchor: "Regla 3 / AC-08", requirement: "One save slot per legacy at a single path: every save always overwrites the same file, never creates a new one elsewhere.", created: 2026-08-21, revised: "", status: active } + - { id: TR-save-007, system: save, gdd: design/gdd/guardado-persistencia.md, domain: Persistence, anchor: "Regla 4 / AC-10", requirement: "Persistence by reference, never by copy: payload contains only era_id, forged relic IDs, pantheon progress counters, and Temporizador elapsed_s/current_phase — no EraDefinition field anywhere.", created: 2026-08-21, revised: "", status: active } + - { id: TR-save-008, system: save, gdd: design/gdd/guardado-persistencia.md, domain: Persistence, anchor: "AC-11", requirement: "The era_id reference is live, not a stale copy: if EraDefinition content changes in a data update, the reloaded save reflects the updated content.", created: 2026-08-21, revised: "", status: active } + - { id: TR-save-009, system: save, gdd: design/gdd/guardado-persistencia.md, domain: Persistence, anchor: "Regla 5 / AC-12/13/14", requirement: "Atomic write: write first to save.tmp and only replace the final file after confirmed success, via atomic rename; the rename is the only op that modifies the final file (no partial observable state).", created: 2026-08-21, revised: "", status: active } + - { id: TR-save-010, system: save, gdd: design/gdd/guardado-persistencia.md, domain: Persistence, anchor: "Regla 6 / AC-18", requirement: "Each save stores schema_version:int compared against CURRENT_SCHEMA_VERSION (starts at 1).", created: 2026-08-21, revised: "", status: active } + - { id: TR-save-011, system: save, gdd: design/gdd/guardado-persistencia.md, domain: Persistence, anchor: "Regla 6 / AC-15/16", requirement: "Migrations run as a linear chain, each responsible only for v->v+1, never skipping versions; a save 3 versions old runs exactly 3 migrations in order.", created: 2026-08-21, revised: "", status: active } + - { id: TR-save-012, system: save, gdd: design/gdd/guardado-persistencia.md, domain: Persistence, anchor: "Regla 6 / AC-17/29", requirement: "If save.schema_version > CURRENT_SCHEMA_VERSION no downgrade is attempted: load blocks with an explicit message, no migration runs, and no 'continue anyway' option is ever presented.", created: 2026-08-21, revised: "", status: active } + - { id: TR-save-013, system: save, gdd: design/gdd/guardado-persistencia.md, domain: Persistence, anchor: "Regla 7 / AC-19/20/21", requirement: "A checksum over the serialized payload is computed on write and stored; on load recomputed and compared. Mismatch -> CORRUPTED state, no auto-repair, no 'ignore and continue'.", created: 2026-08-21, revised: "", status: active } + - { id: TR-save-014, system: save, gdd: design/gdd/guardado-persistencia.md, domain: Persistence, anchor: "AC-30", requirement: "During a load attempt with a failed checksum, NO write operation of any kind occurs against the save file.", created: 2026-08-21, revised: "", status: active } + - { id: TR-save-015, system: save, gdd: design/gdd/guardado-persistencia.md, domain: Persistence, anchor: "Edge Cases 1-2 / AC-24..28", requirement: "Mandatory discrimination between two failure modes: save.tmp write failure -> NOT CORRUPTED (returns to SAVE_EXISTS/NO_SAVE_EXISTS, prior file intact); rename failure -> CORRUPTED.", created: 2026-08-21, revised: "", status: active } + - { id: TR-save-016, system: save, gdd: design/gdd/guardado-persistencia.md, domain: Timing, anchor: "Formulas retry_delay_ms / AC-34/35/36", requirement: "Retry backoff: retry_delay_ms(attempt)=base_delay_ms*backoff_multiplier^(attempt-1); defaults (100/2.0/3) give ~700ms total stall, after which the game halts with a blocking failure state rather than continuing.", created: 2026-08-21, revised: "", status: active } + - { id: TR-save-017, system: save, gdd: design/gdd/guardado-persistencia.md, domain: Timing, anchor: "Edge Case 7 / AC-33", requirement: "Autosaves are serialized: if two consequence events fire in the same frame, the second queues and does not begin until the first SAVING resolves to SAVE_EXISTS or CORRUPTED; no interleaved/parallel write is observable.", created: 2026-08-21, revised: "", status: active } + - { id: TR-save-018, system: save, gdd: design/gdd/guardado-persistencia.md, domain: Persistence, anchor: "Edge Case 6 / AC-32", requirement: "On restart after a crash during SAVING, any orphan save.tmp is detected and discarded unused, and the prior final save loads with SAVE_EXISTS.", created: 2026-08-21, revised: "", status: active } + - { id: TR-save-019, system: save, gdd: design/gdd/guardado-persistencia.md, domain: Ownership, anchor: "Regla 2 / AC-05/31", requirement: "Manual save allowed only in Temporizador PREPARATION/IMMINENT or the Memorial Hall; never during CLIMAX or DEATH_HOLD. If the signal fires anyway it is silently discarded — no state transition, no error dialog, no disk write.", created: 2026-08-21, revised: "", status: active } + - { id: TR-save-020, system: save, gdd: design/gdd/guardado-persistencia.md, domain: Communication, anchor: "UI Requirements 1 / Interactions UI", requirement: "SAVING state must be observable by UI/HUD, which shows an indicator only if the write exceeds save_indicator_display_threshold_ms (default 200ms).", created: 2026-08-21, revised: "", status: active } + - { id: TR-save-021, system: save, gdd: design/gdd/guardado-persistencia.md, domain: Persistence, anchor: "Interactions Temporizador / AC-05/10", requirement: "The Temporizador persists elapsed_s and current_phase captured on the save frame; on reload it resumes from elapsed_s and RECOMPUTES the phase via its F3 — does not trust the persisted phase value.", created: 2026-08-21, revised: "", status: active } + - { id: TR-save-022, system: save, gdd: design/gdd/guardado-persistencia.md, domain: Data, anchor: "States and Transitions / AC-22/23", requirement: "State machine NO_SAVE_EXISTS, SAVING, SAVE_EXISTS, CORRUPTED with observable sequences NO_SAVE_EXISTS->SAVING->SAVE_EXISTS and SAVE_EXISTS->SAVING->SAVE_EXISTS.", created: 2026-08-21, revised: "", status: active } + - { id: TR-save-023, system: save, gdd: design/gdd/guardado-persistencia.md, domain: Data, anchor: "Tuning Knobs / AC-37..44", requirement: "Any knob configured out of safe range (base_delay_ms 50-200, backoff_multiplier 1.5-3.0, max_retries 2-5, save_indicator_display_threshold_ms 150-250) is auto-clamped to the nearest limit at load; the game never fails due to invalid config.", created: 2026-08-21, revised: "", status: active } + - { id: TR-save-024, system: save, gdd: design/gdd/guardado-persistencia.md, domain: Performance, anchor: "Formulas (design note)", requirement: "The write budget assumes the payload is 'a handful of IDs and counters' plus the death payload, so a blocking FileAccess write is sub-ms to a few ms — no own timeout defined; this assumption bounds the schema size.", created: 2026-08-21, revised: "", status: active } + + # ─── CONTROL Y SELECCION DE UNIDADES (Core) — design/gdd/control-y-seleccion-de-unidades.md ─── + - { id: TR-control-001, system: control, gdd: design/gdd/control-y-seleccion-de-unidades.md, domain: Communication, anchor: "Interactions / Dependencies (Input)", requirement: "Consumes Input intent signals (select_requested, box_select_requested, move_commanded, pan_requested, zoom_requested, active_input_method) — a hard interface confirmed bidirectionally.", created: 2026-08-21, revised: "", status: active } + - { id: TR-control-002, system: control, gdd: design/gdd/control-y-seleccion-de-unidades.md, domain: Engine, anchor: "Formula 2 / AC-F3/F4", requirement: "Screen->world transform: world_pos=camera_position_world+(screen_pos_px-viewport_size_px/2)*(zoom/world_unit_scale); Camera2D zoom is INVERSE to magnification (lower=closer) and an inverted sign silently breaks all click->unit resolution.", created: 2026-08-21, revised: "", status: active } + - { id: TR-control-003, system: control, gdd: design/gdd/control-y-seleccion-de-unidades.md, domain: Ownership, anchor: "Regla 5 (+ input OQ-INPUT-2)", requirement: "This system OWNS the Camera2D node and owns the mapping of the design zoom scalar to the Camera2D.zoom property.", created: 2026-08-21, revised: "", status: active } + - { id: TR-control-004, system: control, gdd: design/gdd/control-y-seleccion-de-unidades.md, domain: Data, anchor: "Formula 1 / AC-F1/F2", requirement: "Per-unit select radius: select_radius_world=unit_visual_radius_world+(click_tolerance_px*zoom/world_unit_scale); requires Tropas and Heroes to expose unit_visual_radius_world as a per-type property.", created: 2026-08-21, revised: "", status: active } + - { id: TR-control-005, system: control, gdd: design/gdd/control-y-seleccion-de-unidades.md, domain: Performance, anchor: "Regla 1 / Interactions", requirement: "Resolving select_requested requires a spatial query of units under/near a world point — implies a query interface over positions/hitboxes against Tropas and Heroes.", created: 2026-08-21, revised: "", status: active } + - { id: TR-control-006, system: control, gdd: design/gdd/control-y-seleccion-de-unidades.md, domain: Data, anchor: "Regla 1 / AC-01..05b", requirement: "Selection tiebreak in strict deterministic order: (a) nearest hero within radius, (b) else nearest troop, (c) else deselect all. Priority applies only within-radius, never a global hero-first search.", created: 2026-08-21, revised: "", status: active } + - { id: TR-control-007, system: control, gdd: design/gdd/control-y-seleccion-de-unidades.md, domain: Data, anchor: "Regla 2 / AC-06/07/08", requirement: "Box-select selects every own unit whose CENTER falls inside the rect, inclusive border; includes heroes and troops together (no click hero>troop exclusivity).", created: 2026-08-21, revised: "", status: active } + - { id: TR-control-008, system: control, gdd: design/gdd/control-y-seleccion-de-unidades.md, domain: Ownership, anchor: "Interactions / UI Requirements", requirement: "This system OWNS the current selection set and exposes it to UI/HUD (indicators) and Combate/Dano (set + attack target).", created: 2026-08-21, revised: "", status: active } + - { id: TR-control-009, system: control, gdd: design/gdd/control-y-seleccion-de-unidades.md, domain: Data, anchor: "States and Transitions / AC-14..17", requirement: "Selection state machine NONE_SELECTED, SINGLE_SELECTED, MULTI_SELECTED; camera control is orthogonal and works in all three.", created: 2026-08-21, revised: "", status: active } + - { id: TR-control-010, system: control, gdd: design/gdd/control-y-seleccion-de-unidades.md, domain: Timing, anchor: "Edge Case / AC-18/19", requirement: "Selection auto-clears to NONE_SELECTED the same frame the last selected unit ceases to exist, with no stale reference to a dead unit — requires death/removal notification and freed-reference handling.", created: 2026-08-21, revised: "", status: active } + - { id: TR-control-011, system: control, gdd: design/gdd/control-y-seleccion-de-unidades.md, domain: Communication, anchor: "Regla 4 / AC-10/24/25", requirement: "Only own units selectable; a click on kaiju/esbirro does not add them, but with a prior selection emits an attack-target intent to Combate; with no selection the click is fully ignored (does not deselect).", created: 2026-08-21, revised: "", status: active } + - { id: TR-control-012, system: control, gdd: design/gdd/control-y-seleccion-de-unidades.md, domain: Ownership, anchor: "Regla 3 / AC-09", requirement: "move_commanded sends ALL selected units toward the same resolved world_pos; this system does NOT validate pathfinding — each unit resolves its own (explicit ownership boundary).", created: 2026-08-21, revised: "", status: active } + - { id: TR-control-013, system: control, gdd: design/gdd/control-y-seleccion-de-unidades.md, domain: Data, anchor: "Edge Case / AC-29", requirement: "With NONE_SELECTED a move order is silently ignored: no error, no movement, no state change.", created: 2026-08-21, revised: "", status: active } + - { id: TR-control-014, system: control, gdd: design/gdd/control-y-seleccion-de-unidades.md, domain: Platform, anchor: "Regla 6 / AC-13", requirement: "Virtual gamepad cursor exists only in gamepad mode (via active_input_method), hides immediately on mouse detection, reappears on return to gamepad.", created: 2026-08-21, revised: "", status: active } + - { id: TR-control-015, system: control, gdd: design/gdd/control-y-seleccion-de-unidades.md, domain: Timing, anchor: "Formula 3 / AC-F5", requirement: "Virtual cursor magnetic snap integrates per-frame: cursor_velocity_world=stick_input_dir*stick_input_mag*virtual_cursor_speed+to_unit_dir*snap_strength*virtual_cursor_speed*falloff*alignment, consuming Input Formula 1 symbols.", created: 2026-08-21, revised: "", status: active } + - { id: TR-control-016, system: control, gdd: design/gdd/control-y-seleccion-de-unidades.md, domain: Data, anchor: "AC-F6", requirement: "Property invariant: the pull component never exceeds snap_strength_max(0.55)*virtual_cursor_speed for any distance/alignment/stick_input_mag — the player always retains directional authority.", created: 2026-08-21, revised: "", status: active } + - { id: TR-control-017, system: control, gdd: design/gdd/control-y-seleccion-de-unidades.md, domain: Data, anchor: "Formula 3 / AC-26", requirement: "Gamepad cursor snap resolution reuses the same hero>troop tiebreak of Regla 1, so gamepad and mouse behave identically.", created: 2026-08-21, revised: "", status: active } + - { id: TR-control-018, system: control, gdd: design/gdd/control-y-seleccion-de-unidades.md, domain: Data, anchor: "Regla 5 / AC-27", requirement: "Camera bounded to the active era's map limits, read from Datos de Era; clamp applies at any zoom and never shows area outside the playable map.", created: 2026-08-21, revised: "", status: active } + - { id: TR-control-019, system: control, gdd: design/gdd/control-y-seleccion-de-unidades.md, domain: Data, anchor: "Formula 4 / AC-F7/F8/F9", requirement: "Zoom-compensated panning: effective_pan_speed_world=camera_pan_speed*zoom (full compensation ~1440 px/sec constant); at zoom=1.0 gives exactly 30, consistent with Input AC 23.", created: 2026-08-21, revised: "", status: active } + - { id: TR-control-020, system: control, gdd: design/gdd/control-y-seleccion-de-unidades.md, domain: Timing, anchor: "Edge Case / AC-28", requirement: "When pan_requested and zoom_requested arrive in the same frame, both effects apply independently first and the clamps (zoom [0.5,1.5] and map limits) apply after both — a specified per-frame order of operations, no op skipped.", created: 2026-08-21, revised: "", status: active } + - { id: TR-control-021, system: control, gdd: design/gdd/control-y-seleccion-de-unidades.md, domain: Data, anchor: "Regla 5 / AC-12", requirement: "Zoom clamped to [0.5,1.5] and never exceeds the limits.", created: 2026-08-21, revised: "", status: active } + - { id: TR-control-022, system: control, gdd: design/gdd/control-y-seleccion-de-unidades.md, domain: Ownership, anchor: "Tuning Knobs (consumed constants)", requirement: "Constants consumed from other systems and NOT duplicated: camera_pan_speed=30, zoom_min=0.5, zoom_max=1.5, world_unit_scale=48 (Datos de Era); stick_dead_zone=0.20, zoom_step_fraction=0.10, drag_threshold_px=5 (Input).", created: 2026-08-21, revised: "", status: active } + - { id: TR-control-023, system: control, gdd: design/gdd/control-y-seleccion-de-unidades.md, domain: Data, anchor: "Tuning Knobs (enforcement) / AC-30..33", requirement: "Own knobs (click_tolerance_px, snap_strength, snap_radius, virtual_cursor_speed) are clamped to the nearest limit at load, consistent with the Guardado pattern.", created: 2026-08-21, revised: "", status: active } + - { id: TR-control-024, system: control, gdd: design/gdd/control-y-seleccion-de-unidades.md, domain: Communication, anchor: "Formula 2 (inverse note)", requirement: "The inverse of Formula 2 (px_per_world_unit=world_unit_scale/zoom) is a required symbol for UI/HUD, minimap and VFX to place markers over units.", created: 2026-08-21, revised: "", status: active } + - { id: TR-control-025, system: control, gdd: design/gdd/control-y-seleccion-de-unidades.md, domain: Ownership, anchor: "Overview / Regla 1 (+ input OQ-INPUT-1a)", requirement: "This system guarantees to Pillar 1 that selection is never ambiguous — a hero never receives a fatal order from a wrong click resolution; it is Control's half of the distributed invariant.", created: 2026-08-21, revised: "", status: active } + + # ─── SISTEMA DE TROPAS (Core) — design/gdd/sistema-de-tropas.md ────────────── + - { id: TR-tropas-001, system: tropas, gdd: design/gdd/sistema-de-tropas.md, domain: Data, anchor: "Regla 1 / AC-01", requirement: "Each troop type is a typed Resource (TroopDefinition) with troop_id, troop_name, max_health, move_speed, unit_visual_radius_world, sprite_footprint, and tactical role (melee/ranged/support) — external data, never hardcoded.", created: 2026-08-21, revised: "", status: active } + - { id: TR-tropas-002, system: tropas, gdd: design/gdd/sistema-de-tropas.md, domain: Data, anchor: "Regla 1/2 / AC-01", requirement: "Instances troops by reading the troop portion of the ACTIVE EraDefinition unit_roster, and never instances a type outside that roster.", created: 2026-08-21, revised: "", status: active } + - { id: TR-tropas-003, system: tropas, gdd: design/gdd/sistema-de-tropas.md, domain: Performance, anchor: "Regla 1/2 / Formula C", requirement: "MVP supports 2-3 troop types per civilization with per-era counts starting_count_[type]; the simultaneous-entity budget derives from roster + reinforcements (max_reinforcements <=8 per type).", created: 2026-08-21, revised: "", status: active } + - { id: TR-tropas-004, system: tropas, gdd: design/gdd/sistema-de-tropas.md, domain: Ownership, anchor: "States (note) / Interactions", requirement: "Sistema de Tropas OWNS each troop's current_health and life state (IDLE/MOVING/ENGAGED/DEAD); Combate/Dano only triggers transitions and applies damage, never writes the state directly.", created: 2026-08-21, revised: "", status: active } + - { id: TR-tropas-005, system: tropas, gdd: design/gdd/sistema-de-tropas.md, domain: Ownership, anchor: "Interactions / Formula A / AC-12", requirement: "Each troop exposes position/hitbox and unit_visual_radius_world to Control to resolve the registered select_radius formula owned by Control (value provided, resolution not duplicated).", created: 2026-08-21, revised: "", status: active } + - { id: TR-tropas-006, system: tropas, gdd: design/gdd/sistema-de-tropas.md, domain: Data, anchor: "Regla 5 / AC-05/12", requirement: "Click resolution must guarantee hero>troop priority: every troop unit_visual_radius_world (max 0.4583) stays strictly below the minimum hero radius (0.5).", created: 2026-08-21, revised: "", status: active } + - { id: TR-tropas-007, system: tropas, gdd: design/gdd/sistema-de-tropas.md, domain: Engine, anchor: "Regla 4 / AC-04", requirement: "Each troop resolves its own pathfinding toward the commanded destination via NavigationAgent2D, individually and without formation offset or grouping.", created: 2026-08-21, revised: "", status: active } + - { id: TR-tropas-008, system: tropas, gdd: design/gdd/sistema-de-tropas.md, domain: Communication, anchor: "Regla 4 / AC-07", requirement: "Movement triggered only by a move_commanded event emitted by Control resolved to a world destination — the system does not auto-move troops.", created: 2026-08-21, revised: "", status: active } + - { id: TR-tropas-009, system: tropas, gdd: design/gdd/sistema-de-tropas.md, domain: Engine, anchor: "EC-05 / AC-22", requirement: "Navigation avoidance must organically separate two troops ordered to the same tile without permanent stacking or mutual deadlock, verifiable in headless sim with a timeout assertion.", created: 2026-08-21, revised: "", status: active } + - { id: TR-tropas-010, system: tropas, gdd: design/gdd/sistema-de-tropas.md, domain: Engine, anchor: "EC-04 / AC-21", requirement: "An unreachable destination must resolve to the nearest navigable point and transition the troop to IDLE there, never leaving it MOVING indefinitely.", created: 2026-08-21, revised: "", status: active } + - { id: TR-tropas-011, system: tropas, gdd: design/gdd/sistema-de-tropas.md, domain: Communication, anchor: "Interactions / Regla 6 note", requirement: "Troop receives damage events from Combate that reduce current_health, and notifies ENGAGED/DEAD back — a bidirectional event interface, not direct field access.", created: 2026-08-21, revised: "", status: active } + - { id: TR-tropas-012, system: tropas, gdd: design/gdd/sistema-de-tropas.md, domain: Data, anchor: "Formula D / AC-06", requirement: "Troop death must NOT capture or store any circumstance data (killer_id, location, timestamp, cause) — no such field may exist in the troop schema.", created: 2026-08-21, revised: "", status: active } + - { id: TR-tropas-013, system: tropas, gdd: design/gdd/sistema-de-tropas.md, domain: Ownership, anchor: "Regla 6 / AC-06", requirement: "Troop death path (DEAD) must be separate from hero death path (permadeath/forge): a control test in the same run must prove a hero DOES trigger permadeath and a troop does NOT.", created: 2026-08-21, revised: "", status: active } + - { id: TR-tropas-014, system: tropas, gdd: design/gdd/sistema-de-tropas.md, domain: Data, anchor: "Formula D / AC-10/17/23", requirement: "DEAD is terminal and triggers at the 0-crossing (is_dead=current_health<=0, threshold <= not <); Combate clamps to 0 and negative health is not preserved.", created: 2026-08-21, revised: "", status: active } + - { id: TR-tropas-015, system: tropas, gdd: design/gdd/sistema-de-tropas.md, domain: Communication, anchor: "EC-03 / AC-20", requirement: "A troop reaching DEAD must be removed from Control's selection set the same frame; if it was the last, state goes to NONE_SELECTED.", created: 2026-08-21, revised: "", status: active } + - { id: TR-tropas-016, system: tropas, gdd: design/gdd/sistema-de-tropas.md, domain: Timing, anchor: "EC-07 / AC-24", requirement: "During DEATH_HOLD context, Input suppresses all input so the move_commanded event never reaches this system (upstream suppression, not local filtering).", created: 2026-08-21, revised: "", status: active } + - { id: TR-tropas-017, system: tropas, gdd: design/gdd/sistema-de-tropas.md, domain: Persistence, anchor: "AC-31 / Dependencies", requirement: "Live troop count and state (N per type/state) is part of Guardado's serializable era state, excluding any death circumstance; save+reload restores the same N troops of the same types/states.", created: 2026-08-21, revised: "", status: active } + - { id: TR-tropas-018, system: tropas, gdd: design/gdd/sistema-de-tropas.md, domain: Data, anchor: "Tuning Knobs Enforcement / AC-26..30", requirement: "Out-of-range config values are clamped to the nearest limit AT LOAD (reinforcement_ratio->[0.15,0.35], speed_ratio_troop->[0.10,0.20], min_reinforcements->[1,3], max_reinforcements_hard_cap->[5,12]) — clamp-on-load policy, opposite to Heroes/Combate loud-fail.", created: 2026-08-21, revised: "", status: active } + - { id: TR-tropas-019, system: tropas, gdd: design/gdd/sistema-de-tropas.md, domain: Ownership, anchor: "Tuning Knobs — consumed constants", requirement: "Constants camera_pan_speed=30 and world_unit_scale=48 are owned by Datos de Era and must be consumed from a single registered source, never duplicated here.", created: 2026-08-21, revised: "", status: active } + - { id: TR-tropas-020, system: tropas, gdd: design/gdd/sistema-de-tropas.md, domain: Timing, anchor: "Formula C / AC-15/18", requirement: "Reinforcement cap is per-type (max_reinforcements_[type]=clamp(round(starting_count*reinforcement_ratio), min_reinforcements, max_reinforcements_hard_cap)) and resets per Preparation cycle — requires a queryable Preparation-cycle state.", created: 2026-08-21, revised: "", status: active } + - { id: TR-tropas-021, system: tropas, gdd: design/gdd/sistema-de-tropas.md, domain: Data, anchor: "EC-02 / AC-16/19", requirement: "A type absent from the era roster (starting_count_[type]=0) has reinforcement cap 0 — the min_reinforcements floor does not apply; enforcement is logical (rejection), independent of UI gating.", created: 2026-08-21, revised: "", status: active } + - { id: TR-tropas-022, system: tropas, gdd: design/gdd/sistema-de-tropas.md, domain: Communication, anchor: "Interactions — Kaiju", requirement: "Troops expose their positions as candidates for the kaiju AI threat-target selection (Encuentro con Kaiju Regla 3/F1).", created: 2026-08-21, revised: "", status: active } + - { id: TR-tropas-023, system: tropas, gdd: design/gdd/sistema-de-tropas.md, domain: Communication, anchor: "Visual/Audio — Audio", requirement: "Troops emit signals that Audio listens to (move order, contact, death) — this system provides the trigger, not the sound.", created: 2026-08-21, revised: "", status: active } + - { id: TR-tropas-024, system: tropas, gdd: design/gdd/sistema-de-tropas.md, domain: Communication, anchor: "UI Requirements", requirement: "Troop exposes its life state so UI/HUD renders health bar/indicator and selection outline — this system does not render.", created: 2026-08-21, revised: "", status: active } + - { id: TR-tropas-025, system: tropas, gdd: design/gdd/sistema-de-tropas.md, domain: Engine, anchor: "Visual/Audio — Scale (art bible 8.4)", requirement: "LOD Level 1 only for troops — detail Level 2 rendering is reserved for hero permadeath and troops never activate it.", created: 2026-08-21, revised: "", status: active } + + # ─── SISTEMA DE HEROES (Core) — design/gdd/sistema-de-heroes.md ────────────── + - { id: TR-heroes-001, system: heroes, gdd: design/gdd/sistema-de-heroes.md, domain: Data, anchor: "Regla 1 / AC-H01", requirement: "Each hero is a typed Resource (HeroDefinition) with hero_id, hero_name, max_health, move_speed, unit_visual_radius_world, abilities (Array[AbilityDefinition], >=1), asymmetric_element, relic_category (offensive/rally/defensive).", created: 2026-08-21, revised: "", status: active } + - { id: TR-heroes-002, system: heroes, gdd: design/gdd/sistema-de-heroes.md, domain: Data, anchor: "Regla 1 / AC-H01b", requirement: "Formula inputs sprite_diameter_px, hero_hp_multiplier, pan_speed_fraction are authored fields from which max_health/move_speed/unit_visual_radius_world derive (H1/H2/H3); a definition whose authored stats mismatch its formulas fails loudly at load.", created: 2026-08-21, revised: "", status: active } + - { id: TR-heroes-003, system: heroes, gdd: design/gdd/sistema-de-heroes.md, domain: Data, anchor: "Enforcement 1 / AC-H02/H23", requirement: "Loud-fail-on-load policy (error/assert in dev builds, never silent clamp/substitution) for every out-of-range knob, missing relic_category, and sprite_diameter_px outside [48,96].", created: 2026-08-21, revised: "", status: active } + - { id: TR-heroes-004, system: heroes, gdd: design/gdd/sistema-de-heroes.md, domain: Data, anchor: "Enforcement 2/3 / AC-H23b/c/d", requirement: "Joint config validation at load: base_deliberate+w_stakes+w_purpose must fall in [0.70,1.00] and base_deliberate>=0.35 as a separate individual floor — both loud-fail.", created: 2026-08-21, revised: "", status: active } + - { id: TR-heroes-005, system: heroes, gdd: design/gdd/sistema-de-heroes.md, domain: Ownership, anchor: "Enforcement 4 (test isolation)", requirement: "Knob config loads into an injectable/test-isolated instance, not a global mutable singleton, so a corrupt-config test does not contaminate canonical-formula tests.", created: 2026-08-21, revised: "", status: active } + - { id: TR-heroes-006, system: heroes, gdd: design/gdd/sistema-de-heroes.md, domain: Ownership, anchor: "States / Combate Regla 6", requirement: "Sistema de Heroes OWNS hero life state (ALIVE_IDLE/ALIVE_MOVING/ALIVE_ENGAGED/DYING/DEAD) and health_current; Combate only notifies transitions.", created: 2026-08-21, revised: "", status: active } + - { id: TR-heroes-007, system: heroes, gdd: design/gdd/sistema-de-heroes.md, domain: Timing, anchor: "Formula H5 / AC-H08/H15", requirement: "On crossing health_current<=0 the hero transitions to DYING that same frame and SNAPSHOTS and FREEZES D/T/P at that exact frame, before the sustained beat starts — never re-read in later frames.", created: 2026-08-21, revised: "", status: active } + - { id: TR-heroes-008, system: heroes, gdd: design/gdd/sistema-de-heroes.md, domain: Ownership, anchor: "Formula H5 / AC-H12", requirement: "This system evaluates Formula H4 (relic_quality) at the snapshot frame and adds the already-computed result to the payload; Permadeath transports but never recomputes it.", created: 2026-08-21, revised: "", status: active } + - { id: TR-heroes-009, system: heroes, gdd: design/gdd/sistema-de-heroes.md, domain: Communication, anchor: "Regla 5 / AC-H12", requirement: "Payload exposed to Permadeath on DYING->DEAD contains exactly hero_id, hero_name, relic_category, relic_quality and the snapshotted D/T/P, with no missing or null fields.", created: 2026-08-21, revised: "", status: active } + - { id: TR-heroes-010, system: heroes, gdd: design/gdd/sistema-de-heroes.md, domain: Ownership, anchor: "Formula H4 vars / OQ-3 / Dependencies", requirement: "D and P are provided by Combate as an external classifier; T=1-kaiju_timer_remaining_ratio is read from the Temporizador which OWNS that ratio — this system computes none of the three, only consumes them at the death frame.", created: 2026-08-21, revised: "", status: active } + - { id: TR-heroes-011, system: heroes, gdd: design/gdd/sistema-de-heroes.md, domain: Data, anchor: "Formula H4 / AC-H13/H16", requirement: "H4 is D-gated: relic_quality=clamp(base_accidental+D*(base_deliberate-base_accidental+w_stakes*T+w_purpose*P), 0.0, 1.0), effective output range [0.10,1.00], never 0.", created: 2026-08-21, revised: "", status: active } + - { id: TR-heroes-012, system: heroes, gdd: design/gdd/sistema-de-heroes.md, domain: Data, anchor: "Tier mapping / AC-H19/H20", requirement: "Tier mapping (Tier1 [0.10,0.35), Tier2 [0.35,0.70), Tier3 [0.70,1.00]) is a contract consumed by Forja, with inclusive-lower boundaries and precision enough to distinguish 0.3499 from 0.35 and 0.6999 from 0.70.", created: 2026-08-21, revised: "", status: active } + - { id: TR-heroes-013, system: heroes, gdd: design/gdd/sistema-de-heroes.md, domain: Data, anchor: "Regla 2 / AC-H06", requirement: "relic_category is fixed by definition and never varies with death circumstance; only relic_quality is emergent.", created: 2026-08-21, revised: "", status: active } + - { id: TR-heroes-014, system: heroes, gdd: design/gdd/sistema-de-heroes.md, domain: Data, anchor: "Regla 3 / Formula H3 / V-7 / AC-H07", requirement: "Each hero exposes position/hitbox and unit_visual_radius_world (>=0.5, always > 0.4583 troop ceiling) to Control, sustaining hero>troop priority at hitbox level; hero tiebreak is by proximity to click.", created: 2026-08-21, revised: "", status: active } + - { id: TR-heroes-015, system: heroes, gdd: design/gdd/sistema-de-heroes.md, domain: Communication, anchor: "States / AC-H09", requirement: "Entering DYING triggers Input's DEATH_HOLD context, suppressing all player input until Permadeath releases the beat.", created: 2026-08-21, revised: "", status: active } + - { id: TR-heroes-016, system: heroes, gdd: design/gdd/sistema-de-heroes.md, domain: Ownership, anchor: "States (DYING note) / AC-H11", requirement: "Permadeath — not this system — owns the beat timing and confirms DEAD; a hero stays DYING indefinitely until beat release.", created: 2026-08-21, revised: "", status: active } + - { id: TR-heroes-017, system: heroes, gdd: design/gdd/sistema-de-heroes.md, domain: Timing, anchor: "Edge Cases / AC-H24/H25", requirement: "Permadeath serializes death beats: two heroes entering DYING the same frame snapshot independently but never produce two simultaneous sustained DEATH_HOLDs; the order is defined by Permadeath.", created: 2026-08-21, revised: "", status: active } + - { id: TR-heroes-018, system: heroes, gdd: design/gdd/sistema-de-heroes.md, domain: Persistence, anchor: "Regla 4 / AC-H26", requirement: "Entering DYING triggers a mandatory immediate autosave to Guardado before the flow continues, and the persisted snapshot includes the full death payload explicitly marked 'death pending sealing'.", created: 2026-08-21, revised: "", status: active } + - { id: TR-heroes-019, system: heroes, gdd: design/gdd/sistema-de-heroes.md, domain: Persistence, anchor: "Edge Cases / AC-H27/H27c", requirement: "The load flow (Heroes+Guardado, no Permadeath) coerces a persisted DYING hero to DEAD and forges its relic from the persisted relic_quality, without replaying the visual beat — death is irreversible across a crash.", created: 2026-08-21, revised: "", status: active } + - { id: TR-heroes-020, system: heroes, gdd: design/gdd/sistema-de-heroes.md, domain: Data, anchor: "Regla 4 / AC-H27b", requirement: "DEAD is terminal and unactionable in session: every order (move, Sacrifice), heal effect, or add-to-selection is rejected as a no-op.", created: 2026-08-21, revised: "", status: active } + - { id: TR-heroes-021, system: heroes, gdd: design/gdd/sistema-de-heroes.md, domain: Communication, anchor: "Edge Cases / AC-H10", requirement: "A hero entering DYING is immediately removed from Control's active selection set.", created: 2026-08-21, revised: "", status: active } + - { id: TR-heroes-022, system: heroes, gdd: design/gdd/sistema-de-heroes.md, domain: Ownership, anchor: "Edge Cases / AC-H28 / OQ-1", requirement: "This system does NOT evaluate era victory/defeat — only notifies each individual death to its declared consumers.", created: 2026-08-21, revised: "", status: active } + - { id: TR-heroes-023, system: heroes, gdd: design/gdd/sistema-de-heroes.md, domain: Timing, anchor: "V-5/V-6 note (Permadeath Regla 4 / OQ-P6 #5)", requirement: "The node playing V-5->V-6 (and its particle/audio/tween children) must run exempt from the cooperative game_time_paused (Godot 4.6 PROCESS_MODE_ALWAYS); otherwise animation_done never arrives and every beat falls to safety timeout. (Mechanism reconciled with ADR-0002: real exemption is not-polling-TimeControl; ALWAYS is defensive.)", created: 2026-08-21, revised: "", status: active } + - { id: TR-heroes-024, system: heroes, gdd: design/gdd/sistema-de-heroes.md, domain: Communication, anchor: "Interactions — Permadeath", requirement: "This system notifies end of the resolution animation (V-5->V-6) as animation_done, input to Permadeath Formula P1.", created: 2026-08-21, revised: "", status: active } + - { id: TR-heroes-025, system: heroes, gdd: design/gdd/sistema-de-heroes.md, domain: Timing, anchor: "U-4 / AC-H32c", requirement: "The projected-verdict readout recomputes live pre-death and FREEZES on entering DYING at the snapshotted value, not updating again until it resolves into the relic icon.", created: 2026-08-21, revised: "", status: active } + - { id: TR-heroes-026, system: heroes, gdd: design/gdd/sistema-de-heroes.md, domain: Communication, anchor: "U-3 / AC-H32b/H29", requirement: "Readout visibility is triggered by a projection of time_to_death/incoming_damage_rate_total provided by Encuentro con Kaiju (F2/F6), NOT by current HP — a joint contract, not a local UI knob; the readout reflects the most dangerous source.", created: 2026-08-21, revised: "", status: active } + - { id: TR-heroes-027, system: heroes, gdd: design/gdd/sistema-de-heroes.md, domain: Data, anchor: "U-2 / AC-H32a", requirement: "The readout exposes 3 discrete states that change exactly at tier boundaries (0.35, 0.70), with no intermediate/continuous state.", created: 2026-08-21, revised: "", status: active } + - { id: TR-heroes-028, system: heroes, gdd: design/gdd/sistema-de-heroes.md, domain: Platform, anchor: "U-1 / AC-H32e (art bible 4.5)", requirement: "The readout must communicate tier via three redundant channels (shape/frame, motion, tertiary color) and stay distinguishable under colorblindness or grayscale at LOD Level 1 (24-32px).", created: 2026-08-21, revised: "", status: active } + - { id: TR-heroes-029, system: heroes, gdd: design/gdd/sistema-de-heroes.md, domain: Communication, anchor: "A-4 outcome-blind / AC-H34", requirement: "The decision-window audio cue must be outcome-blind: identical sample on rising cross, falling cross, and D-gate cross — no directional variant or special sting.", created: 2026-08-21, revised: "", status: active } + - { id: TR-heroes-030, system: heroes, gdd: design/gdd/sistema-de-heroes.md, domain: Engine, anchor: "V-4 (art bible 5.4)", requirement: "Two fixed LOD levels (not continuous); Level 2 activates ONLY on the DYING beat, Memorial Hall portrait, or relic reveal — requires 3 Level-2 assets per hero.", created: 2026-08-21, revised: "", status: active } + - { id: TR-heroes-031, system: heroes, gdd: design/gdd/sistema-de-heroes.md, domain: Ownership, anchor: "Tuning Knobs — consumed constants / H1", requirement: "Constants camera_pan_speed=30, world_unit_scale=48, melee_troop_max_health=40 are owned by other systems and consumed registered, never duplicated — changing melee troop HP re-derives hero_max_health.", created: 2026-08-21, revised: "", status: active } + - { id: TR-heroes-032, system: heroes, gdd: design/gdd/sistema-de-heroes.md, domain: Ownership, anchor: "Tuning Knobs / Combate Regla 8", requirement: "sacrifice_radius(3.0) and guard_radius(4.0) are knobs owned by this system but applied by Combate in D/P classification — value ownership separate from the application point.", created: 2026-08-21, revised: "", status: active } + - { id: TR-heroes-033, system: heroes, gdd: design/gdd/sistema-de-heroes.md, domain: Data, anchor: "Edge Cases / AC-H22", requirement: "Overkill does not affect relic_quality: two identical D/T/P scenarios produce identical results regardless of overkill amount (health clamps to 0 in Combate).", created: 2026-08-21, revised: "", status: active } + + # ─── COMBATE/DANO (Feature) — design/gdd/combate-dano.md ───────────────────── + - { id: TR-combate-001, system: combate, gdd: design/gdd/combate-dano.md, domain: Ownership, anchor: "Regla 1 / AC-C01", requirement: "All game damage (unit<->unit and kaiju<->unit) is applied through a single apply_damage(target, amount, source) operation — no other code path mutates health_current.", created: 2026-08-21, revised: "", status: active } + - { id: TR-combate-002, system: combate, gdd: design/gdd/combate-dano.md, domain: Timing, anchor: "Regla 1 / F3 convention", requirement: "Combat resolves on the physics tick (_physics_process, fixed step), never _process, so F3 exact TTKs are reproducible and framerate-independent.", created: 2026-08-21, revised: "", status: active } + - { id: TR-combate-003, system: combate, gdd: design/gdd/combate-dano.md, domain: Timing, anchor: "Regla 1 / AC-C42", requirement: "Each tick resolves in two strict phases: (1) all pending apply_damage against life state at tick start; (2) only then all death notifications (DEAD/DYING) and D/P computation against post-damage world — no hit sees another same-tick hit's result.", created: 2026-08-21, revised: "", status: active } + - { id: TR-combate-004, system: combate, gdd: design/gdd/combate-dano.md, domain: Ownership, anchor: "Regla 2 / AC-C03", requirement: "CombatProfile (owned by this system) holds attack_damage(int), attack_range, attack_cooldown_s, aggro_range, leash_range per unit type (troop, hero, kaiju); Tropas/Heroes deliberately do NOT own combat stats.", created: 2026-08-21, revised: "", status: active } + - { id: TR-combate-005, system: combate, gdd: design/gdd/combate-dano.md, domain: Data, anchor: "Regla 2 / AC-C03", requirement: "CombatProfile is data-driven: changing the .tres changes behavior without touching code; no combat value is hardcoded.", created: 2026-08-21, revised: "", status: active } + - { id: TR-combate-006, system: combate, gdd: design/gdd/combate-dano.md, domain: Data, anchor: "F1 / AC-C02/C17/C18", requirement: "F1: health_after=clamp(health_before-attack_damage, 0, max_health) — health never negative and overkill discarded.", created: 2026-08-21, revised: "", status: active } + - { id: TR-combate-007, system: combate, gdd: design/gdd/combate-dano.md, domain: Timing, anchor: "Regla 3 / AC-C43", requirement: "Attack cooldown never resets on losing or switching target and keeps running in NO_TARGET (anti target-switch exploit).", created: 2026-08-21, revised: "", status: active } + - { id: TR-combate-008, system: combate, gdd: design/gdd/combate-dano.md, domain: Timing, anchor: "Regla 3 / AC-C05/C43b", requirement: "On target acquisition an acquisition floor applies: the first hit never lands before a full attack_cooldown_s from the acquisition instant, even with an inherited cooldown already at 0.", created: 2026-08-21, revised: "", status: active } + - { id: TR-combate-009, system: combate, gdd: design/gdd/combate-dano.md, domain: Engine, anchor: "Regla 3", requirement: "No traveling projectiles in MVP: instant impact on hit resolution; the ranged role is modeled with larger attack_range, not projectile physics.", created: 2026-08-21, revised: "", status: active } + - { id: TR-combate-010, system: combate, gdd: design/gdd/combate-dano.md, domain: Engine, anchor: "Edge Cases / AC-C48", requirement: "attack_range is pure distance, no line-of-sight check in MVP (a ranged unit can fire through obstacles).", created: 2026-08-21, revised: "", status: active } + - { id: TR-combate-011, system: combate, gdd: design/gdd/combate-dano.md, domain: Performance, anchor: "Regla 4 / AC-C06", requirement: "Auto-aggro selects the NEAREST valid enemy within aggro_range — not first-detected nor random; requires a deterministic proximity query.", created: 2026-08-21, revised: "", status: active } + - { id: TR-combate-012, system: combate, gdd: design/gdd/combate-dano.md, domain: Data, anchor: "Regla 4 / AC-C07b", requirement: "The hold position is fixed at the instant of entering TARGET_ACQUIRED, not updated during chase, and re-fixed on each new acquisition (including after chaining a kill) — requires per-combatant position state.", created: 2026-08-21, revised: "", status: active } + - { id: TR-combate-013, system: combate, gdd: design/gdd/combate-dano.md, domain: Data, anchor: "Regla 4 / AC-C07", requirement: "A unit exceeding leash_range from its hold position abandons the chase, returns to hold, and transitions to NO_TARGET.", created: 2026-08-21, revised: "", status: active } + - { id: TR-combate-014, system: combate, gdd: design/gdd/combate-dano.md, domain: Communication, anchor: "Regla 4 / AC-C08/C09/C45", requirement: "Combate reads explicit Control orders (attack-target, attack-move) that override auto-acquisition: attack-target consumes on target death (reverts to auto-aggro), attack-move continues toward destination auto-engaging.", created: 2026-08-21, revised: "", status: active } + - { id: TR-combate-015, system: combate, gdd: design/gdd/combate-dano.md, domain: Data, anchor: "States and Transitions", requirement: "Combate models a per-combatant engagement cycle (NO_TARGET/TARGET_ACQUIRED/ATTACKING) separate from unit life cycle, applied identically to troops, heroes, and kaiju.", created: 2026-08-21, revised: "", status: active } + - { id: TR-combate-016, system: combate, gdd: design/gdd/combate-dano.md, domain: Ownership, anchor: "Regla 6 / AC-C11/C12", requirement: "Combate does NOT own health or life state: on transition to TARGET_ACQUIRED/ATTACKING it only invokes the owning system's notification method (ENGAGED/ALIVE_ENGAGED) and never writes the state field directly.", created: 2026-08-21, revised: "", status: active } + - { id: TR-combate-017, system: combate, gdd: design/gdd/combate-dano.md, domain: Data, anchor: "Regla 7 / AC-C13/C-NEG", requirement: "Troop death: DEAD without capturing or exposing killer_id, location, timestamp or cause — no circumstance field may exist on that path.", created: 2026-08-21, revised: "", status: active } + - { id: TR-combate-018, system: combate, gdd: design/gdd/combate-dano.md, domain: Timing, anchor: "Regla 7/8 / AC-C14", requirement: "Hero death: DYING + Combate computes and FREEZES D and P at the same 0-crossing frame, without re-reading them in later frames.", created: 2026-08-21, revised: "", status: active } + - { id: TR-combate-019, system: combate, gdd: design/gdd/combate-dano.md, domain: Ownership, anchor: "Regla 8 / AC-C35", requirement: "Combate is a pure classifier: it only READS documented getters from Control (sacrifice_command_active, sacrifice_command_destination) and Kaiju (marked_targets/marked_threats) — never issues a Sacrifice command, never marks, and does not cache D/P between frames.", created: 2026-08-21, revised: "", status: active } + - { id: TR-combate-020, system: combate, gdd: design/gdd/combate-dano.md, domain: Data, anchor: "Regla 8 / AC-C28..C31", requirement: "D=1 iff all three hold (Sacrifice command confirmed, active/not-cancelled at death frame, death within sacrifice_radius=3.0 of ordered destination); otherwise D=0.", created: 2026-08-21, revised: "", status: active } + - { id: TR-combate-021, system: combate, gdd: design/gdd/combate-dano.md, domain: Data, anchor: "Regla 8 / AC-C32/C33/C34", requirement: "P=1 by either of two equivalent paths: death within guard_radius=4.0 of a marked target/ally (defensive) or neutralization of a marked critical kaiju threat (offensive).", created: 2026-08-21, revised: "", status: active } + - { id: TR-combate-022, system: combate, gdd: design/gdd/combate-dano.md, domain: Ownership, anchor: "Regla 8 / AC-C36", requirement: "Combate does NOT compute T and its payload cannot include any T/kaiju_timer_remaining_ratio field — that input is provided by the Temporizador and read by Heroes.", created: 2026-08-21, revised: "", status: active } + - { id: TR-combate-023, system: combate, gdd: design/gdd/combate-dano.md, domain: Communication, anchor: "AC-C50", requirement: "The D/P hand-off to Heroes occurs at the same 0-crossing frame so Heroes can combine it with T and compute relic_quality with no inter-system latency.", created: 2026-08-21, revised: "", status: active } + - { id: TR-combate-024, system: combate, gdd: design/gdd/combate-dano.md, domain: Ownership, anchor: "Regla 9 / AC-C16/C47", requirement: "Combate does NOT evaluate era victory/defeat: neither the kaiju reaching 0 nor the last death emits an end-of-era evaluation signal or call.", created: 2026-08-21, revised: "", status: active } + - { id: TR-combate-025, system: combate, gdd: design/gdd/combate-dano.md, domain: Data, anchor: "Enforcement / F5 / AC-C26/C27/C37..39", requirement: "Loud-fail-on-load (no clamp) for attack_damage>=1, attack_cooldown_s>=0.1, no negatives, and invariant F5 attack_range<=aggro_range<=leash_range.", created: 2026-08-21, revised: "", status: active } + - { id: TR-combate-026, system: combate, gdd: design/gdd/combate-dano.md, domain: Data, anchor: "F5 named exception / AC-C27b", requirement: "Named loader exception: the <=leash_range leg of F5 is not evaluated for the Kaiju while its leash_range is the sentinel provisional* (Kaiju nullifies the field entirely); the attack_range<=aggro_range leg keeps normal loud-fail.", created: 2026-08-21, revised: "", status: active } + - { id: TR-combate-027, system: combate, gdd: design/gdd/combate-dano.md, domain: Data, anchor: "Edge Cases / AC-C40", requirement: "apply_damage on an already-DEAD unit or an already-DYING hero is a no-op — does not decrement health nor re-fire the death notification.", created: 2026-08-21, revised: "", status: active } + - { id: TR-combate-028, system: combate, gdd: design/gdd/combate-dano.md, domain: Data, anchor: "Edge Cases / AC-C41", requirement: "No friendly fire in MVP: apply_damage applies only between opposing factions — requires a per-unit faction concept queryable at the application point.", created: 2026-08-21, revised: "", status: active } + - { id: TR-combate-029, system: combate, gdd: design/gdd/combate-dano.md, domain: Timing, anchor: "Edge Cases / AC-C52", requirement: "Combat cooldowns follow game-time scale, not real time: if the DEATH_HOLD beat pauses time (game_time_paused, now owned by TimeControl per ADR-0002, read by Combate), no cooldown advances.", created: 2026-08-21, revised: "2026-08-21", status: active } + - { id: TR-combate-030, system: combate, gdd: design/gdd/combate-dano.md, domain: Communication, anchor: "CU-1", requirement: "Combate exposes per unit current_health/max_health, engagement state (NO_TARGET/TARGET_ACQUIRED/ATTACKING), and damage events for UI/HUD and VFX consumption.", created: 2026-08-21, revised: "", status: active } + - { id: TR-combate-031, system: combate, gdd: design/gdd/combate-dano.md, domain: Communication, anchor: "CU-2 / CV-1", requirement: "Each hit resolution emits an impact event (who hit whom, how much) that UI/HUD and VFX hook for hit feedback; Combate provides the trigger, not the render.", created: 2026-08-21, revised: "", status: active } + - { id: TR-combate-032, system: combate, gdd: design/gdd/combate-dano.md, domain: Ownership, anchor: "CU-3 / AC-C51", requirement: "Combate exposes recent raw incoming-damage rate per unit as an input; it does NOT compute time_to_death — that projection is owned by Encuentro con Kaiju.", created: 2026-08-21, revised: "", status: active } + - { id: TR-combate-033, system: combate, gdd: design/gdd/combate-dano.md, domain: Communication, anchor: "CU-4", requirement: "Combate exposes no verdict text or outcome judgments — only numeric state and events.", created: 2026-08-21, revised: "", status: active } + - { id: TR-combate-034, system: combate, gdd: design/gdd/combate-dano.md, domain: Timing, anchor: "CV-3", requirement: "On notifying DYING, the hero's engagement state freezes with it that frame: no attacker/target keeps animating an exchange against an already-DYING unit.", created: 2026-08-21, revised: "", status: active } + - { id: TR-combate-035, system: combate, gdd: design/gdd/combate-dano.md, domain: Timing, anchor: "CV-4", requirement: "The kaiju attack (attack_cooldown_s=4.5) must telegraph before landing, giving legible reaction time — the telegraph is a data/timing requirement, not just aesthetic.", created: 2026-08-21, revised: "", status: active } + - { id: TR-combate-036, system: combate, gdd: design/gdd/combate-dano.md, domain: Ownership, anchor: "Tuning Knobs — consumed constants", requirement: "HP constants (melee_infantry=40, ranged_troop=20, support_troop=28, heroes 120-200) and radii sacrifice_radius=3.0/guard_radius=4.0 are owned by Tropas/Heroes and consumed without duplication; changing HP there rebalances combat here.", created: 2026-08-21, revised: "", status: active } + - { id: TR-combate-037, system: combate, gdd: design/gdd/combate-dano.md, domain: Engine, anchor: "CV-2", requirement: "Troop death triggers the LOD-Level-1 fall/dissolve animation with the same duration as any state transition — no camera pause, no dedicated light, no desaturation (any VFX giving it pause is rejected in review).", created: 2026-08-21, revised: "", status: active } + - { id: TR-combate-038, system: combate, gdd: design/gdd/combate-dano.md, domain: Ownership, anchor: "CV-3 / CA-3", requirement: "Combate triggers the hero death crossing but does not stage the beat: the sustained staging is Permadeath's; Combate only ducks its own combat bed when the beat fires.", created: 2026-08-21, revised: "", status: active } + + # ─── ENCUENTRO CON KAIJU (Feature) — design/gdd/encuentro-con-kaiju.md ─────── + - { id: TR-kaiju-001, system: kaiju, gdd: design/gdd/encuentro-con-kaiju.md, domain: Communication, anchor: "Regla 1 / AC-K01a/b", requirement: "Instances the ACTIVE-era kaiju the same frame it receives entered_climax from Temporizador, never before.", created: 2026-08-21, revised: "", status: active } + - { id: TR-kaiju-002, system: kaiju, gdd: design/gdd/encuentro-con-kaiju.md, domain: Data, anchor: "Regla 1 / AC-K02", requirement: "The kaiju CombatProfile (attack_damage, attack_range, attack_cooldown_s, aggro_range) is populated exclusively from the kaiju_definition resource of Datos de Era — changing the .tres changes behavior without code.", created: 2026-08-21, revised: "", status: active } + - { id: TR-kaiju-003, system: kaiju, gdd: design/gdd/encuentro-con-kaiju.md, domain: Engine, anchor: "Regla 2 / AC-K05b/K06", requirement: "The kaiju leash_range field is never read: pursuit is unlimited during CLIMAX and only ends by target death, exit of aggro_range without valid candidate, or reselection via Regla 3.", created: 2026-08-21, revised: "", status: active } + - { id: TR-kaiju-004, system: kaiju, gdd: design/gdd/encuentro-con-kaiju.md, domain: Engine, anchor: "Regla 3 / F1 / AC-K07/K16", requirement: "Kaiju target selection uses F1 threat score (threat_damage_weight*recent_damage_dealt + hero_threat_bonus*is_hero + random_variation), overriding Combate's proximity aggro for the kaiju only; there is never a silent distance fallback.", created: 2026-08-21, revised: "", status: active } + - { id: TR-kaiju-005, system: kaiju, gdd: design/gdd/encuentro-con-kaiju.md, domain: Data, anchor: "F1 (recent_damage_dealt algorithm)", requirement: "recent_damage_dealt requires, per candidate, a list of damage events (timestamp_climax_s, amount) with lazy pruning over a fixed threat_window_s sliding window; decayed bucket sums allowed but NOT a continuous exponential average.", created: 2026-08-21, revised: "", status: active } + - { id: TR-kaiju-006, system: kaiju, gdd: design/gdd/encuentro-con-kaiju.md, domain: Timing, anchor: "F1 / AC-K09", requirement: "Damage-event accumulation for F1 occurs continuously in all intent-cycle states; only the EVALUATION of threat_score is restricted to entering SEEKING.", created: 2026-08-21, revised: "", status: active } + - { id: TR-kaiju-007, system: kaiju, gdd: design/gdd/encuentro-con-kaiju.md, domain: Engine, anchor: "Regla 8 / AC-K08/K12/K13", requirement: "Esbirros use Combate's standard engagement cycle (proximity aggro, no telegraph) and never enter SEEKING/TELEGRAPHING/STRIKING/STAGGERED — the AI-architecture asymmetry between kaiju and esbirros is deliberate.", created: 2026-08-21, revised: "", status: active } + - { id: TR-kaiju-008, system: kaiju, gdd: design/gdd/encuentro-con-kaiju.md, domain: Ownership, anchor: "Regla 4 / States", requirement: "The system owns its own FSM — the intent cycle SEEKING->TELEGRAPHING->STRIKING->SEEKING (+STAGGERED) — layered OVER Combate's separate engagement FSM (NO_TARGET/TARGET_ACQUIRED/ATTACKING), which it does not own or modify.", created: 2026-08-21, revised: "", status: active } + - { id: TR-kaiju-009, system: kaiju, gdd: design/gdd/encuentro-con-kaiju.md, domain: Engine, anchor: "Regla 4 / AC-K61", requirement: "Attack gate: while the kaiju is in SEEKING/TELEGRAPHING, base-FSM attack resolution is suppressed and its attack_cooldown_s does not run, even if the target is in range with cooldown ready.", created: 2026-08-21, revised: "", status: active } + - { id: TR-kaiju-010, system: kaiju, gdd: design/gdd/encuentro-con-kaiju.md, domain: Timing, anchor: "Regla 4 (override) / AC-K75", requirement: "Entering STRIKING invokes apply_damage directly and synchronously that same frame, skipping the base-FSM acquisition floor and full-cooldown wait for that single call; attack_cooldown_s begins counting after that hit.", created: 2026-08-21, revised: "", status: active } + - { id: TR-kaiju-011, system: kaiju, gdd: design/gdd/encuentro-con-kaiju.md, domain: Ownership, anchor: "States (hard requirement) / AC-K87", requirement: "Single-owner hard requirement: the kaiju must be fully excluded from Combate's generic auto-attack loop — the intent controller is the ONLY caller of apply_damage for the kaiju in all states; can_attack() is no longer consulted for it. No reliance on _physics_process order between siblings.", created: 2026-08-21, revised: "", status: active } + - { id: TR-kaiju-012, system: kaiju, gdd: design/gdd/encuentro-con-kaiju.md, domain: Timing, anchor: "States (hard req) / AC-K88", requirement: "The STRIKING handler must synchronously (same handler) check whether the kaiju crossed 0 HP before advancing to SEEKING or reselecting, and abort immediately; no reliance on death-signal fan-out order.", created: 2026-08-21, revised: "", status: active } + - { id: TR-kaiju-013, system: kaiju, gdd: design/gdd/encuentro-con-kaiju.md, domain: Engine, anchor: "States ('for the ADR') / Regla 9", requirement: "The apply_damage signal/notification connection from Combate must be immediate mode, never CONNECT_DEFERRED, so Regla 9 'victory in that instant' is synchronous.", created: 2026-08-21, revised: "", status: active } + - { id: TR-kaiju-014, system: kaiju, gdd: design/gdd/encuentro-con-kaiju.md, domain: Communication, anchor: "Regla 5 / AC-K43/K44/K45", requirement: "On entering TELEGRAPHING the system writes marked_target (defensive path) and marked_threat (offensive path) to Combate for P computation, and clears them immediately on any state exit — never persisting between telegraphs.", created: 2026-08-21, revised: "", status: active } + - { id: TR-kaiju-015, system: kaiju, gdd: design/gdd/encuentro-con-kaiju.md, domain: Communication, anchor: "KU-1 / AC-K46", requirement: "Exit from TELEGRAPHING exposes the reason as a three-case enum — (a) STRIKING, (b) STAGGERED, (c) cancellation by death/aggro exit — as part of the mark-clear event.", created: 2026-08-21, revised: "", status: active } + - { id: TR-kaiju-016, system: kaiju, gdd: design/gdd/encuentro-con-kaiju.md, domain: Communication, anchor: "Regla 6 / F2 / AC-K23", requirement: "The system exposes per hero time_to_death=telegraph_duration_s+(kill_time_hits_kaiju*attack_cooldown_s_kaiju) with hero_HP_at_telegraph_start snapshotted on entering TELEGRAPHING and never re-read during the telegraph.", created: 2026-08-21, revised: "", status: active } + - { id: TR-kaiju-017, system: kaiju, gdd: design/gdd/encuentro-con-kaiju.md, domain: Data, anchor: "AC-K78 / Edge Cases", requirement: "When the kaiju is not telegraphing against a hero, time_to_death is exposed as an explicit 'not applicable / no kaiju telegraph' state — never a 0 or infinity sentinel.", created: 2026-08-21, revised: "", status: active } + - { id: TR-kaiju-018, system: kaiju, gdd: design/gdd/encuentro-con-kaiju.md, domain: Communication, anchor: "Regla 6b / F6 / AC-K67", requirement: "The system exposes per hero incoming_damage_rate_total = kaiju_projected_dps(hero) + sum esbirro_dps(e) over each esbirro currently engaged to that hero, whenever the hero is under attack.", created: 2026-08-21, revised: "", status: active } + - { id: TR-kaiju-019, system: kaiju, gdd: design/gdd/encuentro-con-kaiju.md, domain: Ownership, anchor: "KU-2 / OQ-14", requirement: "The system exposes time_to_death and incoming_damage_rate_total as raw per-frame unsmoothed values; anti-flicker smoothing/hysteresis is the consumer's responsibility.", created: 2026-08-21, revised: "", status: active } + - { id: TR-kaiju-020, system: kaiju, gdd: design/gdd/encuentro-con-kaiju.md, domain: Data, anchor: "Regla 7 / F1 note / AC-K35/K36", requirement: "The neutralization counter (Regla 7) is a distinct accumulator with no shared state with the F1 sliding window: it resets to 0 on each SEEKING->TELEGRAPHING transition and admits no residual damage from earlier phases.", created: 2026-08-21, revised: "", status: active } + - { id: TR-kaiju-021, system: kaiju, gdd: design/gdd/encuentro-con-kaiju.md, domain: Data, anchor: "F3 note / AC-K31", requirement: "The esbirro spawner keeps last_spawned_wave_count and instances only the delta (wave_count(t)-last_spawned_wave_count)*esbirro_wave_size — never wave_count(t)*esbirro_wave_size.", created: 2026-08-21, revised: "", status: active } + - { id: TR-kaiju-022, system: kaiju, gdd: design/gdd/encuentro-con-kaiju.md, domain: Performance, anchor: "Regla 8b / F3 gate / AC-K69", requirement: "Concurrency gate: spawnable=min((wave_count(t)-last_spawned_wave_count)*esbirro_wave_size, max(0, max_concurrent_esbirros-alive_esbirro_count)); a partially-spawned wave keeps its remainder PENDING (deferred, not discarded) and last_spawned_wave_count only advances on full waves.", created: 2026-08-21, revised: "", status: active } + - { id: TR-kaiju-023, system: kaiju, gdd: design/gdd/encuentro-con-kaiju.md, domain: Performance, anchor: "Tuning Knobs / AC-K84", requirement: "On-screen concurrent entity budget: max_concurrent_esbirros recommended 0-16, 17-24 unvalidated technical max; simultaneous player troops/heroes count against the same screen budget.", created: 2026-08-21, revised: "", status: active } + - { id: TR-kaiju-024, system: kaiju, gdd: design/gdd/encuentro-con-kaiju.md, domain: Ownership, anchor: "Regla 9 / AC-K49/K51/K52/K53", requirement: "This system OWNS era victory/defeat evaluation: victory when the kaiju crosses 0 HP, defeat when no hero remains ALIVE_* (troops are not a condition), both declared the same frame with absolute priority over any intent-cycle state.", created: 2026-08-21, revised: "", status: active } + - { id: TR-kaiju-025, system: kaiju, gdd: design/gdd/encuentro-con-kaiju.md, domain: Communication, anchor: "KU-4 / AC-K57/K60", requirement: "The era verdict is emitted as a single discrete event (once per verdict), not a per-frame repeated signal, consumed by Transicion de Era and UI/HUD.", created: 2026-08-21, revised: "", status: active } + - { id: TR-kaiju-026, system: kaiju, gdd: design/gdd/encuentro-con-kaiju.md, domain: Data, anchor: "Regla 10 / F5 / AC-K64", requirement: "ENRAGED is not an FSM state but a latched monotone orthogonal flag that only alters effective values (telegraph_duration_s_effective, attack_damage_effective) via F5; it introduces no new transitions and never reverts to false within a CLIMAX.", created: 2026-08-21, revised: "", status: active } + - { id: TR-kaiju-027, system: kaiju, gdd: design/gdd/encuentro-con-kaiju.md, domain: Data, anchor: "Regla 10b / F5 / AC-K63b/K86", requirement: "Fury trigger (b) requires per hero a last_combat_damage_climax_s (timestamp of last damage dealt to kaiju OR esbirro) and evaluating each frame engaged_hero_count(t)=30; kaiju_attack_damage*neutralize_hits_equivalent>telegraph_duration_s*max_single_troop_dps unless allow_single_troop_neutralize=true) are implemented as a dedicated validate() called immediately after ResourceLoader.load() that propagates an Error; all era loading must be centralized in one function so no call-site skips the check.", created: 2026-08-21, revised: "", status: active } + - { id: TR-kaiju-037, system: kaiju, gdd: design/gdd/encuentro-con-kaiju.md, domain: Communication, anchor: "KU-1 / AC-K55/K58/K77", requirement: "The system exposes to UI/HUD throughout CLIMAX: intent-cycle state, ENRAGED flag, marked_target/marked_threat identity, and telegraph progress as a continuous float [0.0,1.0] (not a 1/2/3 third index — the consumer derives the third).", created: 2026-08-21, revised: "", status: active } + - { id: TR-kaiju-038, system: kaiju, gdd: design/gdd/encuentro-con-kaiju.md, domain: Communication, anchor: "KA-1 / AC-K59", requirement: "The Audio contract requires a sub-state hook (on_telegraph_progress) firing at each third boundary, plus a state-transition hook (on_intent_transition) and a Fury-entry hook (on_enrage) — a from_state/to_state hook cannot express the three internal thirds of TELEGRAPHING.", created: 2026-08-21, revised: "", status: active } + - { id: TR-kaiju-039, system: kaiju, gdd: design/gdd/encuentro-con-kaiju.md, domain: Data, anchor: "KU-3 / AC-K56", requirement: "No payload exposed by this system contains editorialized text ('Watch out!'/'Danger!') — only numeric/enumerated state and discrete events.", created: 2026-08-21, revised: "", status: active } + - { id: TR-kaiju-040, system: kaiju, gdd: design/gdd/encuentro-con-kaiju.md, domain: Performance, anchor: "KV-1 / art bible 3.4", requirement: "Scale contrast: the kaiju must win the visual hierarchy by default and its telegraph escalation be legible from any battle camera zoom, with units at 24-32px and max on-screen clutter.", created: 2026-08-21, revised: "", status: active } + - { id: TR-kaiju-041, system: kaiju, gdd: design/gdd/encuentro-con-kaiju.md, domain: Engine, anchor: "KV-5", requirement: "Hitstop of 2-4 frames scaled by target type (hero/troop) accompanies ONLY the kaiju STRIKING hits — never routine troop/esbirro hits, and no camera pause.", created: 2026-08-21, revised: "", status: active } + - { id: TR-kaiju-042, system: kaiju, gdd: design/gdd/encuentro-con-kaiju.md, domain: Persistence, anchor: "KV-5", requirement: "Each non-neutralized STRIKING adds a permanent, cumulative crack to the screen-border motif, persistent throughout the current era.", created: 2026-08-21, revised: "", status: active } + - { id: TR-kaiju-043, system: kaiju, gdd: design/gdd/encuentro-con-kaiju.md, domain: Platform, anchor: "KV-1b / KV-1", requirement: "Mandatory non-auditory peripheral channel: screen-border vignette pulse + gamepad rumble, scaling on the same third rhythm as the kaiju body from third 1, perceptible with eyes anywhere on screen.", created: 2026-08-21, revised: "", status: active } + - { id: TR-kaiju-044, system: kaiju, gdd: design/gdd/encuentro-con-kaiju.md, domain: Platform, anchor: "KV-10", requirement: "Photosensitivity limit: the luminous pulse (kaiju body and KV-1b vignette) never exceeds 3 flashes/sec at its fastest, including the Fury-compressed form; if the rate derived from telegraph_duration_s_effective would cross it, urgency is expressed by sustained bloom intensity/extent. A reduced-flash toggle must replace the pulse with a continuous ramp preserving the three-third read.", created: 2026-08-21, revised: "", status: active } + + # ─── TEMPORIZADOR DE PREPARACION/RITUAL (Core) — design/gdd/temporizador-de-preparacion-ritual.md ─── + - { id: TR-temporizador-001, system: temporizador, gdd: design/gdd/temporizador-de-preparacion-ritual.md, domain: Data, anchor: "Regla 1 / AC-T01", requirement: "All timing config (approach_duration_s, imminent_window_s) is read from the ACTIVE EraDefinition; no timing value is hardcoded — changing the .tres changes behavior.", created: 2026-08-21, revised: "", status: active } + - { id: TR-temporizador-002, system: temporizador, gdd: design/gdd/temporizador-de-preparacion-ritual.md, domain: Data, anchor: "Regla 2 / F3 / AC-T02", requirement: "There is a SINGLE clock (elapsed_s) and the three phases are thresholds over it, not separate clocks; elapsed_s is continuous across any phase crossing (never resets or jumps).", created: 2026-08-21, revised: "", status: active } + - { id: TR-temporizador-003, system: temporizador, gdd: design/gdd/temporizador-de-preparacion-ritual.md, domain: Ownership, anchor: "Regla 3 / AC-T03/T03b", requirement: "This system is the sole owner and source of truth of kaiju_timer_remaining_ratio in [0,1]; Kaiju and Heroes consume it read-only and no consumer may modify it or touch the internal elapsed_s.", created: 2026-08-21, revised: "", status: active } + - { id: TR-temporizador-004, system: temporizador, gdd: design/gdd/temporizador-de-preparacion-ritual.md, domain: Ownership, anchor: "Regla 4 / AC-T04", requirement: "No gameplay action (move troops, invoke resources, hero commands) can alter elapsed_s or phase — there is no write path from gameplay (no acceleration, pause, or reverse).", created: 2026-08-21, revised: "", status: active } + - { id: TR-temporizador-005, system: temporizador, gdd: design/gdd/temporizador-de-preparacion-ritual.md, domain: Timing, anchor: "Regla 4b / AC-T04b", requirement: "elapsed_s is monotone non-decreasing during active play: each successive read across consecutive frames is always >= the previous.", created: 2026-08-21, revised: "", status: active } + - { id: TR-temporizador-006, system: temporizador, gdd: design/gdd/temporizador-de-preparacion-ritual.md, domain: Communication, anchor: "Regla 5 / AC-T05", requirement: "current_phase and time_remaining_to_climax_s are queryable and correct per F1/F3 on ANY frame while the era is ACTIVE — never null or stale.", created: 2026-08-21, revised: "", status: active } + - { id: TR-temporizador-007, system: temporizador, gdd: design/gdd/temporizador-de-preparacion-ritual.md, domain: Timing, anchor: "Edge Cases / AC-T19", requirement: "Phase-change signals entered_imminent and entered_climax are emitted in order and within the same time step even if one large delta crosses PREPARATION directly to CLIMAX — no consumer can miss entered_imminent to a framerate hitch.", created: 2026-08-21, revised: "", status: active } + - { id: TR-temporizador-008, system: temporizador, gdd: design/gdd/temporizador-de-preparacion-ritual.md, domain: Timing, anchor: "Regla 6 / AC-T06/T20", requirement: "On entering CLIMAX, kaiju_timer_remaining_ratio fixes exactly at 0.0 and stays there indefinitely, and internal elapsed_s FREEZES at approach_duration_s (avoids unbounded growth) regardless of F1/F2 clamp guards.", created: 2026-08-21, revised: "", status: active } + - { id: TR-temporizador-009, system: temporizador, gdd: design/gdd/temporizador-de-preparacion-ritual.md, domain: Ownership, anchor: "Regla 7 / AC-T07/T07b", requirement: "Public surface is strictly pacing: entered_imminent/entered_climax signals and phase/ratio/time/imminent_local_progress getters — no instancing, AI or damage methods; the system must complete a PREPARATION->IMMINENT->CLIMAX cycle instanced in isolation, with no combat system present and no missing-dependency error.", created: 2026-08-21, revised: "", status: active } + - { id: TR-temporizador-010, system: temporizador, gdd: design/gdd/temporizador-de-preparacion-ritual.md, domain: Timing, anchor: "Edge Cases / AC-T21", requirement: "elapsed_s accumulates game-time delta (affected by pause and time scale), never real time or frame count: two sims with the same total game time in different step sizes (60x1/60s vs 30x1/30s) produce identical elapsed_s, phase and ratio. (Game-time source now TimeControl per ADR-0002.)", created: 2026-08-21, revised: "2026-08-21", status: active } + - { id: TR-temporizador-011, system: temporizador, gdd: design/gdd/temporizador-de-preparacion-ritual.md, domain: Timing, anchor: "Regla 4 / Edge Cases / AC-T22", requirement: "The global menu pause freezes elapsed_s while real time passes, and on unpause the clock resumes exactly from its held value — menu pause is not a gameplay path to stop the clock. (NOTE: menu pause via SceneTree.paused; its interaction with TimeControl is an open item for the TimeControl/save-persistence ADRs.)", created: 2026-08-21, revised: "", status: active } + - { id: TR-temporizador-012, system: temporizador, gdd: design/gdd/temporizador-de-preparacion-ritual.md, domain: Persistence, anchor: "Edge Cases / AC-T23 / Dependencies", requirement: "elapsed_s and current phase are part of savable state; on reload the phase is deterministically RECOMPUTED from elapsed_s via F3 and never trusts the persisted phase value (even if desynced).", created: 2026-08-21, revised: "", status: active } + - { id: TR-temporizador-013, system: temporizador, gdd: design/gdd/temporizador-de-preparacion-ritual.md, domain: Persistence, anchor: "Edge Cases (autosave) / AC-T23", requirement: "On autosave (e.g. hero death) or manual save during any phase, elapsed_s and phase are captured on that same frame, consistent with the Heroes snapshot reading kaiju_timer_remaining_ratio at the death frame.", created: 2026-08-21, revised: "", status: active } + - { id: TR-temporizador-014, system: temporizador, gdd: design/gdd/temporizador-de-preparacion-ritual.md, domain: Data, anchor: "Edge Cases / AC-T24", requirement: "Before the era is ACTIVE the clock does not run and the system returns an explicit documented 'not active' state, never a misleading 1.0/PREPARATION suggesting an encounter in progress.", created: 2026-08-21, revised: "", status: active } + - { id: TR-temporizador-015, system: temporizador, gdd: design/gdd/temporizador-de-preparacion-ritual.md, domain: Ownership, anchor: "Regla 6 / States / AC-T25", requirement: "CLIMAX is terminal for this system: it never transitions out of it by itself and stays with ratio=0.0 indefinitely even if no external system evaluates the era's end — victory/defeat evaluation is NOT owned by this system.", created: 2026-08-21, revised: "", status: active } + - { id: TR-temporizador-016, system: temporizador, gdd: design/gdd/temporizador-de-preparacion-ritual.md, domain: Data, anchor: "Tuning Knobs Enforcement / AC-T17/T18/T26/T27", requirement: "Three distinct config-validation tiers at EraDefinition load: (1) structural constraints (approach_duration_s>0, 0 always loud-fail, no clamp; (2) safe feel ranges ([300,1800],[30,90]) -> loud-fail in dev builds; (3) the ratio imminent_window_s<0.20*approach_duration_s is advisory and load must complete without error if violated.", created: 2026-08-21, revised: "", status: active } + - { id: TR-temporizador-017, system: temporizador, gdd: design/gdd/temporizador-de-preparacion-ritual.md, domain: Timing, anchor: "F3 (boundary convention) / AC-T13..T16", requirement: "Inclusive-lower boundary convention in F3: at elapsed_s==imminent_threshold_s the phase is already IMMINENT and at elapsed_s==approach_duration_s already CLIMAX, with no ambiguous frame between phases.", created: 2026-08-21, revised: "", status: active } + - { id: TR-temporizador-018, system: temporizador, gdd: design/gdd/temporizador-de-preparacion-ritual.md, domain: Data, anchor: "Formulas (float comparison) / AC-T11/T11b", requirement: "Non-integer float comparisons use a +-0.0001 tolerance (never strict equality) because several F4 ratios are non-terminating fractions not exactly representable in binary float.", created: 2026-08-21, revised: "", status: active } + - { id: TR-temporizador-019, system: temporizador, gdd: design/gdd/temporizador-de-preparacion-ritual.md, domain: Communication, anchor: "F4 / VA-1 / U-2", requirement: "The system exposes imminent_local_progress in [0.0,1.0] (derived, monotone, meaningful only during IMMINENT) as a continuous signal for Audio crescendo ramps and UI visual escalation, without exposing raw seconds.", created: 2026-08-21, revised: "", status: active } + + # ─── PERMADEATH (Feature) — design/gdd/permadeath.md ───────────────────────── + - { id: TR-permadeath-001, system: permadeath, gdd: design/gdd/permadeath.md, domain: Data, anchor: "Regla 1 / AC-P01/P02", requirement: "Permadeath receives from Heroes a circumstance payload with exactly hero_id, hero_name, relic_category, relic_quality (already computed by H4) and the snapshotted D/T/P, and never computes, recomputes or mutates any of them — only transports.", created: 2026-08-21, revised: "", status: active } + - { id: TR-permadeath-002, system: permadeath, gdd: design/gdd/permadeath.md, domain: Ownership, anchor: "Regla 5 / AC-P15", requirement: "Writing the hero state field is owned by Heroes; Permadeath only invokes the DEAD-confirmation method, never writes the field directly.", created: 2026-08-21, revised: "", status: active } + - { id: TR-permadeath-003, system: permadeath, gdd: design/gdd/permadeath.md, domain: Ownership, anchor: "Regla 4 / AC-P12", requirement: "Permadeath owns and publishes the boolean game_time_paused, set true on a beat entering HOLDING and back to false on RESOLVED; Combate (AC-C52), Kaiju (Regla 11/AC-K89) and potentially the Temporizador read it. (Superseded by ADR-0002: TimeControl now owns the regime; Permadeath requests PAUSED.)", created: 2026-08-21, revised: "2026-08-21", status: active } + - { id: TR-permadeath-004, system: permadeath, gdd: design/gdd/permadeath.md, domain: Timing, anchor: "Regla 4 / AC-P07b", requirement: "Global pause is cooperative by flag polling/notification — Permadeath never sets SceneTree.paused=true; each consumer decides not to advance its logic while it reads game_time_paused==true.", created: 2026-08-21, revised: "", status: active } + - { id: TR-permadeath-005, system: permadeath, gdd: design/gdd/permadeath.md, domain: Communication, anchor: "OQ-P5 sub-decisions", requirement: "Publication of game_time_paused must be hybrid: a game_time_paused_changed(bool) signal for edge transitions PLUS a boolean getter for late-subscribing consumers (pure per-frame polling is fragile).", created: 2026-08-21, revised: "", status: active } + - { id: TR-permadeath-006, system: permadeath, gdd: design/gdd/permadeath.md, domain: Timing, anchor: "Edge Cases / Formula P1 / AC-P07", requirement: "The node managing the beat runs on a clock exempt from global pause (PROCESS_MODE_ALWAYS), accumulating elapsed_s by UNSCALED delta; if it ran on the paused scale the beat would deadlock. (Mechanism reconciled with ADR-0002: exemption = not-polling-TimeControl; ALWAYS is defensive.)", created: 2026-08-21, revised: "", status: active } + - { id: TR-permadeath-007, system: permadeath, gdd: design/gdd/permadeath.md, domain: Engine, anchor: "Regla 4 corollary / cross-GDD #5", requirement: "The Heroes resolution-animation node V-5->V-6 (and its particle/audio/tween children) must run PROCESS_MODE_ALWAYS, or animation_done never arrives and every death falls to the timeout path.", created: 2026-08-21, revised: "", status: active } + - { id: TR-permadeath-008, system: permadeath, gdd: design/gdd/permadeath.md, domain: Timing, anchor: "Formula P1 / AC-P04/P05/P06", requirement: "Beat-release condition is release=(elapsed_s>=death_hold_min_duration_s) AND (animation_done OR elapsed_s>=death_hold_timeout_s), evaluated each tick while a beat is HOLDING.", created: 2026-08-21, revised: "", status: active } + - { id: TR-permadeath-009, system: permadeath, gdd: design/gdd/permadeath.md, domain: Communication, anchor: "Formula P1 / OQ-P2", requirement: "A signal/hook animation_done from Heroes is required to notify end of the V-5->V-6 resolution animation; its exact form (Godot signal with/without payload, callback) is not fixed.", created: 2026-08-21, revised: "", status: active } + - { id: TR-permadeath-010, system: permadeath, gdd: design/gdd/permadeath.md, domain: Communication, anchor: "Regla 2 / PV-5 / AC-P06b", requirement: "On the timeout path, Permadeath must emit to Heroes/Forja a 'force reveal to end-state' signal (force_relic_reveal_final(), provisional) before/with the hand-off — no RESOLVED can occur without the relic icon shown in its final state.", created: 2026-08-21, revised: "", status: active } + - { id: TR-permadeath-011, system: permadeath, gdd: design/gdd/permadeath.md, domain: Data, anchor: "Regla 3 / AC-P10", requirement: "Permadeath keeps a FIFO queue of pending deaths with deterministic order (arrival + tiebreak by smallest hero_id), reproducible every run; the order is not configurable.", created: 2026-08-21, revised: "", status: active } + - { id: TR-permadeath-012, system: permadeath, gdd: design/gdd/permadeath.md, domain: Data, anchor: "Regla 3 / AC-P09/P11", requirement: "Exactly one beat is HOLDING at any instant; other entries stay QUEUED with their frozen immutable payload while waiting.", created: 2026-08-21, revised: "", status: active } + - { id: TR-permadeath-013, system: permadeath, gdd: design/gdd/permadeath.md, domain: Ownership, anchor: "States (header note)", requirement: "The per-entry queue state (QUEUED->HOLDING->RESOLVED) is distinct from the hero state (DYING/DEAD, owned by Heroes) — two separate state machines with distinct owners.", created: 2026-08-21, revised: "", status: active } + - { id: TR-permadeath-014, system: permadeath, gdd: design/gdd/permadeath.md, domain: Timing, anchor: "Regla 8 / AC-P21/P21b/P21c", requirement: "Floor compression of an N-death chain is piso_efectivo=clamp(death_hold_total_lockout_cap_s/N, death_hold_min_compressed_s, death_hold_min_duration_s), uniform for all beats of the chain, with N FROZEN at chain-formation frame and never recomputed.", created: 2026-08-21, revised: "", status: active } + - { id: TR-permadeath-015, system: permadeath, gdd: design/gdd/permadeath.md, domain: Data, anchor: "Regla 8 (algorithm)", requirement: "The implementation must assert N>=1 before dividing, to fail loudly on an invocation bug rather than divide by zero.", created: 2026-08-21, revised: "", status: active } + - { id: TR-permadeath-016, system: permadeath, gdd: design/gdd/permadeath.md, domain: Data, anchor: "Regla 8 / Edge Cases / AC-P21c", requirement: "A death arriving after the first beat entered HOLDING is treated as a NEW chain with its own cap budget — never folded into the in-progress chain by raising its N.", created: 2026-08-21, revised: "", status: active } + - { id: TR-permadeath-017, system: permadeath, gdd: design/gdd/permadeath.md, domain: Communication, anchor: "Regla 8 / AC-P08c / Dependencies", requirement: "Permadeath reads N_max via EraDefinition.get_hero_roster_count() on the era-LOADED signal, never in its own _ready() (avoids the race against the LOADING->LOADED machine); it is its only cross-system read.", created: 2026-08-21, revised: "", status: active } + - { id: TR-permadeath-018, system: permadeath, gdd: design/gdd/permadeath.md, domain: Timing, anchor: "Regla 8 / AC-P08c", requirement: "Cap feasibility validation re-runs per era (each time an era reaches LOADED), not once at global startup, because the roster varies by civilization/era.", created: 2026-08-21, revised: "", status: active } + - { id: TR-permadeath-019, system: permadeath, gdd: design/gdd/permadeath.md, domain: Data, anchor: "Formula P1 constraint / AC-P08/P08b", requirement: "All config validation fails loudly at load, never clamped/substituted silently: death_hold_timeout_s>=death_hold_min_duration_s+death_hold_min_animation_window_s; cap>=death_hold_min_duration_s; cap>=N_max*death_hold_min_compressed_s; death_hold_min_compressed_s<=death_hold_min_duration_s.", created: 2026-08-21, revised: "", status: active } + - { id: TR-permadeath-020, system: permadeath, gdd: design/gdd/permadeath.md, domain: Data, anchor: "AC-P08b", requirement: "The cap-feasibility validation function must accept N_max as an injected parameter, decoupled from the actual Datos de Era read, to be unit-testable without a loaded era.", created: 2026-08-21, revised: "", status: active } + - { id: TR-permadeath-021, system: permadeath, gdd: design/gdd/permadeath.md, domain: Data, anchor: "Edge Cases / AC-P03", requirement: "A payload with any missing or null field (hero_id, hero_name, relic_category, D/T/P) fails loudly on receipt in a dev build; never substitutes a silent default.", created: 2026-08-21, revised: "", status: active } + - { id: TR-permadeath-022, system: permadeath, gdd: design/gdd/permadeath.md, domain: Persistence, anchor: "Regla 6 / cross-GDD #2", requirement: "The autosave persisted at DYING (fired by Heroes AC-H26, not Permadeath) must include the full death payload incl. relic_quality, marked 'death pending sealing' — not just state=DYING.", created: 2026-08-21, revised: "", status: active } + - { id: TR-permadeath-023, system: permadeath, gdd: design/gdd/permadeath.md, domain: Persistence, anchor: "Regla 8b / AC-P18-ext", requirement: "On reload, the load flow (Heroes+Guardado, not Permadeath) coerces DYING->DEAD and forges the relic from the persisted relic_quality, without resuming or replaying the visual beat.", created: 2026-08-21, revised: "", status: active } + - { id: TR-permadeath-024, system: permadeath, gdd: design/gdd/permadeath.md, domain: Persistence, anchor: "Regla 8b / AC-P18", requirement: "Permadeath persists no runtime queue state; on init after a crash its queue starts empty, with no restored HOLDING/QUEUED states and no resume call.", created: 2026-08-21, revised: "", status: active } + - { id: TR-permadeath-025, system: permadeath, gdd: design/gdd/permadeath.md, domain: Engine, anchor: "OQ-P5 sub-question / Gap 2026-08-15", requirement: "An explicit Permadeath.reset_queue() (or equivalent) must exist, invoked by every load sequence that returns control to the player — not only by Transicion de Era — because Godot 4.6 provides no clean 'scene changed' hook an Autoload can passively listen to.", created: 2026-08-21, revised: "", status: active } + - { id: TR-permadeath-026, system: permadeath, gdd: design/gdd/permadeath.md, domain: Timing, anchor: "Gap 2026-08-15 / PU-2", requirement: "The invariant is 'the Permadeath queue is empty at every point where the game returns control to the player from a loaded state' — otherwise Input stays permanently suppressed with no real beat behind it.", created: 2026-08-21, revised: "", status: active } + - { id: TR-permadeath-027, system: permadeath, gdd: design/gdd/permadeath.md, domain: Communication, anchor: "Regla 5 / AC-P16", requirement: "The hand-off to Forja delivers hero_id, hero_name, relic_category, relic_quality exactly once per hero, synchronously with beat release, via the provisional symbol receive_relic(...).", created: 2026-08-21, revised: "", status: active } + - { id: TR-permadeath-028, system: permadeath, gdd: design/gdd/permadeath.md, domain: Data, anchor: "AC-P17/P17b", requirement: "Exactly-once idempotency is implemented by removing the queue entry BEFORE invoking DEAD confirmation and hand-off; re-evaluating an already-resolved entry (_evaluate_release() with the same entry_id) is a no-op.", created: 2026-08-21, revised: "", status: active } + - { id: TR-permadeath-029, system: permadeath, gdd: design/gdd/permadeath.md, domain: Platform, anchor: "Regla 4 / PU-2 / AC-P07c", requirement: "Permadeath controls release of the Input DEATH_HOLD context; suppression includes the open-global-pause-menu action, so no real engine pause can start during a beat and no RESOLVED occurs with SceneTree.paused==true.", created: 2026-08-21, revised: "", status: active } + - { id: TR-permadeath-030, system: permadeath, gdd: design/gdd/permadeath.md, domain: Ownership, anchor: "Reglas 6-7 / AC-P19/P20", requirement: "Permadeath does not call Guardado (does not duplicate the autosave) and emits no era victory/defeat signal (that evaluation is owned by Encuentro con Kaiju).", created: 2026-08-21, revised: "", status: active } + - { id: TR-permadeath-031, system: permadeath, gdd: design/gdd/permadeath.md, domain: Ownership, anchor: "PU-1 / PU-2", requirement: "Permadeath has no own HUD; during HOLDING it does not update or redraw the Heroes relic_quality readout (already frozen by AC-H32c/U-4) and exposes no skip/cancel control.", created: 2026-08-21, revised: "", status: active } + - { id: TR-permadeath-032, system: permadeath, gdd: design/gdd/permadeath.md, domain: Engine, anchor: "Regla 3 (QUEUED presentation) / AC-P22", requirement: "A QUEUED hero does not play its collapse animation (V-5); it holds the last living/frozen pose and its full V-5->V-6 beat starts only on entering HOLDING.", created: 2026-08-21, revised: "", status: active } + + # ─── FORJA DE LEGADO (Feature) — design/gdd/forja-de-legado.md ─────────────── + - { id: TR-forja-001, system: forja, gdd: design/gdd/forja-de-legado.md, domain: Communication, anchor: "Regla 1 / AC-FL02", requirement: "receive_relic(hero_id, hero_name, relic_category, relic_quality) is the ONLY creation entry point of Forja; it exposes no other method and listens to no signal that would accept a troop death as input.", created: 2026-08-21, revised: "", status: active } + - { id: TR-forja-002, system: forja, gdd: design/gdd/forja-de-legado.md, domain: Data, anchor: "Regla 3 / AC-FL08", requirement: "RelicRecord contains exactly these fields: relic_id, source_hero_id, source_hero_name, relic_category, relic_quality, tier, origin_era_id, forge_index, epitaph.", created: 2026-08-21, revised: "", status: active } + - { id: TR-forja-003, system: forja, gdd: design/gdd/forja-de-legado.md, domain: Data, anchor: "Regla 2 / AC-FL07", requirement: "Forja stores BOTH relic_quality (exact float, byte-identical to received, never recomputed) and tier (derived discrete band) in the same record.", created: 2026-08-21, revised: "", status: active } + - { id: TR-forja-004, system: forja, gdd: design/gdd/forja-de-legado.md, domain: Data, anchor: "Formula FL1 / AC-FL04/05/06", requirement: "FL1 maps relic_quality->tier with inclusive lower bounds: Delgada [0.10,0.35), Solida [0.35,0.70), Legendaria [0.70,1.00]; the cuts are owned by Heroes and Forja does not duplicate them as a knob.", created: 2026-08-21, revised: "", status: active } + - { id: TR-forja-005, system: forja, gdd: design/gdd/forja-de-legado.md, domain: Data, anchor: "Formula FL1 (epsilon boundary)", requirement: "FL1 must compare boundaries with a tolerance EPSILON_TIER=1e-6; a value within +-EPSILON_TIER of a cut classifies to the UPPER tier (because relic_quality is float-computed in H4, not authored).", created: 2026-08-21, revised: "", status: active } + - { id: TR-forja-006, system: forja, gdd: design/gdd/forja-de-legado.md, domain: Data, anchor: "Regla 4 / OQ-FL5 / AC-FL12", requirement: "relic_id is of type String, unique across the WHOLE legacy (global to the save, not per-era), and stable across saves/loads; the concrete encoding from forge_index is implementation.", created: 2026-08-21, revised: "", status: active } + - { id: TR-forja-007, system: forja, gdd: design/gdd/forja-de-legado.md, domain: Data, anchor: "Regla 4 / AC-FL12", requirement: "relic_id is derived deterministically and uniquely from forge_index, NEVER from Godot get_instance_id() (instance IDs recycle and do not survive save/load) — enforced by a static CI grep.", created: 2026-08-21, revised: "", status: active } + - { id: TR-forja-008, system: forja, gdd: design/gdd/forja-de-legado.md, domain: Persistence, anchor: "Formula FL2 / AC-FL10/FL11", requirement: "forge_counter is a monotone counter PERSISTED in the save, starting at 0 in a new legacy, continuing from the persisted value after reload, and never resetting between eras nor reusing an index.", created: 2026-08-21, revised: "", status: active } + - { id: TR-forja-009, system: forja, gdd: design/gdd/forja-de-legado.md, domain: Persistence, anchor: "Edge Cases / OQ-FL4 point 3", requirement: "If int64 fidelity is required for forge_counter, serialization must use FileAccess.store_var/get_var — JSON decodes every number as float/double (53 bits) and does not guarantee 64 bits; the Guardado save format is not yet specified.", created: 2026-08-21, revised: "", status: active } + - { id: TR-forja-010, system: forja, gdd: design/gdd/forja-de-legado.md, domain: Ownership, anchor: "Source-of-truth note / Regla 6", requirement: "Forja is the SINGLE canonical source of truth of a relic's data; Guardado persists by reference (IDs + fields), Datos de Era references via legacy_props, and Pantheon/Reliquias only read — none duplicates or redefines the RelicRecord.", created: 2026-08-21, revised: "", status: active } + - { id: TR-forja-011, system: forja, gdd: design/gdd/forja-de-legado.md, domain: Timing, anchor: "Regla 6 / AC-FL15", requirement: "Strict order on the deposit path: the relic is added to the in-memory registry BEFORE the autosave is triggered, verifiable with ordered spies.", created: 2026-08-21, revised: "", status: active } + - { id: TR-forja-012, system: forja, gdd: design/gdd/forja-de-legado.md, domain: Timing, anchor: "OQ-FL4 point 1 (mandatory ADR scope)", requirement: "The registry mutation and the autosave trigger must occur in the SAME call frame, with no call_deferred() or CONNECT_DEFERRED on that path — otherwise the order becomes non-deterministic between frames.", created: 2026-08-21, revised: "", status: active } + - { id: TR-forja-013, system: forja, gdd: design/gdd/forja-de-legado.md, domain: Threading, anchor: "OQ-FL4 point 4 / Regla 6 / AC-FL16", requirement: "'The game does not continue until SAVE_EXISTS' must be implemented as an await on the Guardado completion signal (yields to the main loop, keeps the 'saving...' indicator rendering), NEVER as a synchronous thread block nor OS.delay_msec().", created: 2026-08-21, revised: "", status: active } + - { id: TR-forja-014, system: forja, gdd: design/gdd/forja-de-legado.md, domain: Engine, anchor: "OQ-FL4 point 2", requirement: "Loud-fails validating trust-boundary-crossing data (incomplete payload, relic_quality out of range, unknown relic_category, incomplete epitaph_templates) cannot rely on assert() alone (compiled out in exported builds): they require assert() PAIRED with an if-guard + push_error() + a safe-failure path that stays active in release.", created: 2026-08-21, revised: "", status: active } + - { id: TR-forja-015, system: forja, gdd: design/gdd/forja-de-legado.md, domain: Data, anchor: "Regla 5 / AC-FL14", requirement: "A relic_quality outside [0.10,1.00] fails loudly — never clamped to produce an out-of-band tier or a 'zero' relic.", created: 2026-08-21, revised: "", status: active } + - { id: TR-forja-016, system: forja, gdd: design/gdd/forja-de-legado.md, domain: Data, anchor: "Edge Cases / AC-FL23", requirement: "A relic_category outside {offensive, defensive, rally} fails loudly; Forja never invents or substitutes a default category.", created: 2026-08-21, revised: "", status: active } + - { id: TR-forja-017, system: forja, gdd: design/gdd/forja-de-legado.md, domain: Data, anchor: "Regla 8 / AC-FL21", requirement: "Defensive deduplication uses the key (source_hero_id, origin_era_id); a second delivery is a no-op logged with a dev-build warning, never a silent duplicate with two relic_id.", created: 2026-08-21, revised: "", status: active } + - { id: TR-forja-018, system: forja, gdd: design/gdd/forja-de-legado.md, domain: Data, anchor: "Regla 8 / AC-FL26", requirement: "On era activation (LOADED), Forja (or the load validator) validates that the hero roster's hero_id are unique; a duplicate makes the era activation fail loudly, naming the colliding hero_id.", created: 2026-08-21, revised: "", status: active } + - { id: TR-forja-019, system: forja, gdd: design/gdd/forja-de-legado.md, domain: Communication, anchor: "Regla 6 / Edge Cases / AC-FL24", requirement: "Forja reads from Datos de Era which era_id is ACTIVE to tag origin_era_id; with no ACTIVE era it fails loudly, never assigning a default or null origin_era_id.", created: 2026-08-21, revised: "", status: active } + - { id: TR-forja-020, system: forja, gdd: design/gdd/forja-de-legado.md, domain: Data, anchor: "AC-FL24 / AC-FL26", requirement: "The ACTIVE-era read must be injectable (ActiveEraProvider seam stubbed, same pattern as Permadeath AC-P08b) to force the 'no era' case without a real era loaded.", created: 2026-08-21, revised: "", status: active } + - { id: TR-forja-021, system: forja, gdd: design/gdd/forja-de-legado.md, domain: Persistence, anchor: "OQ-FL4", requirement: "The RelicRecord is mutable persisted state (not immutable authored data like EraDefinition), so it probably lives serialized inside the Guardado save, not as an authored .tres — an architecture decision pending.", created: 2026-08-21, revised: "", status: active } + - { id: TR-forja-022, system: forja, gdd: design/gdd/forja-de-legado.md, domain: Data, anchor: "Tuning Knobs / AC-FL09", requirement: "epitaph derives from the epitaph_templates table (dict category x tier -> string, exactly 9 entries); a missing key or empty string fails loudly at table load, naming the missing key(s).", created: 2026-08-21, revised: "", status: active } + - { id: TR-forja-023, system: forja, gdd: design/gdd/forja-de-legado.md, domain: Communication, anchor: "AC-FL19 / AC-FL20", requirement: "The relic registry must expose a full-list read hook (list_relics()->[RelicRecord], provisional) and a combat-inputs hook (get_relic_combat_inputs()->{relic_category, tier}, provisional) without requiring consumers to read relic_quality or internal fields.", created: 2026-08-21, revised: "", status: active } + - { id: TR-forja-024, system: forja, gdd: design/gdd/forja-de-legado.md, domain: Communication, anchor: "Regla 7 / AC-FL18", requirement: "Forja reaches PERSISTED with no consumer of Reliquias (#12) or Pantheon (#14) connected — structural decoupling, verifiable.", created: 2026-08-21, revised: "", status: active } + - { id: TR-forja-025, system: forja, gdd: design/gdd/forja-de-legado.md, domain: Data, anchor: "States / AC-FL17", requirement: "Per-relic state machine RECEIVED->FORGED->DEPOSITED->PERSISTED, with commit points marked for failure handling; a blocking autosave failure leaves the relic at DEPOSITED with no Forja-own retry.", created: 2026-08-21, revised: "", status: active } + - { id: TR-forja-026, system: forja, gdd: design/gdd/forja-de-legado.md, domain: Persistence, anchor: "Edge Cases / AC-FL25", requirement: "After a crash between RESOLVED and end of autosave, the load flow re-forges the relic from the persisted DYING payload; dedup guarantees exactly one relic per hero — never zero, never two.", created: 2026-08-21, revised: "", status: active } + - { id: TR-forja-027, system: forja, gdd: design/gdd/forja-de-legado.md, domain: Persistence, anchor: "Interactions (Transicion de Era) / AC-FL11", requirement: "The relic registry is part of the legacy that persists BETWEEN eras (Transicion de Era carries it across civilizations); forge_counter never resets at that crossing.", created: 2026-08-21, revised: "", status: active } + - { id: TR-forja-028, system: forja, gdd: design/gdd/forja-de-legado.md, domain: Ownership, anchor: "FU-1 / FU-2", requirement: "Forja exposes no screen, prompt, or readout during play; the epitaph is provenance data consumers render in their own context.", created: 2026-08-21, revised: "", status: active } + - { id: TR-forja-029, system: forja, gdd: design/gdd/forja-de-legado.md, domain: Engine, anchor: "FV-1 / FV-2", requirement: "Forja guarantees each RelicRecord references a resolvable icon determined by relic_category (base silhouette) + tier (gilded finish grade); it does not animate the reveal nor compose the scene.", created: 2026-08-21, revised: "", status: active } + + # ─── RELIQUIAS/BENDICIONES (Feature) — design/gdd/reliquias-bendiciones.md ─── + - { id: TR-reliquias-001, system: reliquias, gdd: design/gdd/reliquias-bendiciones.md, domain: Ownership, anchor: "Regla 1 / AC-RB01/RB02", requirement: "The pool is built by reading Forja's RelicRecord registry read-only (relic_id, relic_category, tier, source_hero_name, epitaph, forge_index); this system never forges, modifies, or deletes a relic.", created: 2026-08-21, revised: "", status: active } + - { id: TR-reliquias-002, system: reliquias, gdd: design/gdd/reliquias-bendiciones.md, domain: Ownership, anchor: "Source-of-truth note", requirement: "This system keeps only ephemeral invocation state (remaining charges, active effects, current offering, 'in draw pool' flag); it duplicates no legacy data.", created: 2026-08-21, revised: "", status: active } + - { id: TR-reliquias-003, system: reliquias, gdd: design/gdd/reliquias-bendiciones.md, domain: Data, anchor: "F-RB1 / AC-RB30", requirement: "effective_stat(u,s,t)=base_stat(u_type,s)+sum magnitude(m) over active modifiers; base_stat is read read-only from the type's CombatProfile/HeroDefinition, never mutated.", created: 2026-08-21, revised: "", status: active } + - { id: TR-reliquias-004, system: reliquias, gdd: design/gdd/reliquias-bendiciones.md, domain: Communication, anchor: "F-RB1 / OQ-RB1 / AC-RB32", requirement: "Combate must expose a new read-hook effective_attack_damage(instance)->int layering the instance modifier stack over the type CombatProfile at hit-resolution time; it does not exist in combate-dano.md today. (Landed by ADR-0001.)", created: 2026-08-21, revised: "2026-08-21", status: active } + - { id: TR-reliquias-005, system: reliquias, gdd: design/gdd/reliquias-bendiciones.md, domain: Data, anchor: "Regla 8 / AC-RB27", requirement: "Every bonus is applied as a temporary instance modifier keyed by (relic_id, unit_instance_id) — bound to the concrete instance, never the type/definition. (Landed by ADR-0001.)", created: 2026-08-21, revised: "2026-08-21", status: active } + - { id: TR-reliquias-006, system: reliquias, gdd: design/gdd/reliquias-bendiciones.md, domain: Ownership, anchor: "Regla 8 / AC-RB26", requirement: "Never writes CombatProfile/HeroDefinition/TroopDefinition persistent; all modification lives in the ephemeral modifier layer — enforced by static CI grep plus the surface AC. (Landed by ADR-0001.)", created: 2026-08-21, revised: "2026-08-21", status: active } + - { id: TR-reliquias-007, system: reliquias, gdd: design/gdd/reliquias-bendiciones.md, domain: Data, anchor: "F-RB1 / AC-RB31/RB31b", requirement: "Stacking: additive across distinct relics; one active instance per relic_id per unit — re-picking an already-active relic REFRESHES its expiry_tick to a full new window without changing magnitude or creating a second instance. (Landed by ADR-0001.)", created: 2026-08-21, revised: "2026-08-21", status: active } + - { id: TR-reliquias-008, system: reliquias, gdd: design/gdd/reliquias-bendiciones.md, domain: Data, anchor: "F-RB1 / AC-RB31b", requirement: "Max simultaneous bonus is computable and bounded: max_stat_bonus(s)=sum of up to D distinct highest-magnitude relics of the stat, D=min(invocation_charges, distinct relics of stat in pool, number fitting in effect_duration_s); not runtime-clamped.", created: 2026-08-21, revised: "", status: active } + - { id: TR-reliquias-009, system: reliquias, gdd: design/gdd/reliquias-bendiciones.md, domain: Data, anchor: "Regla 4 / AC-RB13", requirement: "No damage bonus is a percentage multiplier; all are flat additive +N applied as a temporary instance modifier (Combate Regla 5). (Landed by ADR-0001.)", created: 2026-08-21, revised: "2026-08-21", status: active } + - { id: TR-reliquias-010, system: reliquias, gdd: design/gdd/reliquias-bendiciones.md, domain: Communication, anchor: "Regla 4/8 / AC-RB14", requirement: "Damaging effects use Combate's EXISTING single-target apply_damage path; an area hit is implemented as multiple single-target apply_damage calls over enemies in radius — never a new damage formula or a cleave hook.", created: 2026-08-21, revised: "", status: active } + - { id: TR-reliquias-011, system: reliquias, gdd: design/gdd/reliquias-bendiciones.md, domain: Data, anchor: "F-RB2 / AC-RB07..RB10 / Regla 3", requirement: "magnitude(base_value, tier)=round_half_up(base_value*tier_factor(tier)) with tier_factor(Delgada)=1.0, Solida=solid_factor, Legendaria=legendary_factor; tier scales only magnitude, never effect type.", created: 2026-08-21, revised: "", status: active } + - { id: TR-reliquias-012, system: reliquias, gdd: design/gdd/reliquias-bendiciones.md, domain: Data, anchor: "F-RB2 (loud-fail invariant) / AC-RB11", requirement: "Loud-fail invariant at load: magnitude strictly increasing across the 3 tiers for each base_value in use (catches rounding collapse, e.g. base_value=1, solid_factor=1.3).", created: 2026-08-21, revised: "", status: active } + - { id: TR-reliquias-013, system: reliquias, gdd: design/gdd/reliquias-bendiciones.md, domain: Data, anchor: "Loud-fail invariants (load) / AC-RB42..RB45", requirement: "Loud-fail invariants at load (never silent clamp): draw_size>=1; invocation_charges>=1; duration_s>0; solid_factor>1.0; legendary_factor>solid_factor; base_value>=1; pool_cap>=1; 0<=max_legendary_per_draw<=draw_size.", created: 2026-08-21, revised: "", status: active } + - { id: TR-reliquias-014, system: reliquias, gdd: design/gdd/reliquias-bendiciones.md, domain: Data, anchor: "Regla 6 / F-RB3 / AC-RB18/RB23", requirement: "The draw uses an injectable/seed-controllable RNG (same discipline as Kaiju AC-K71); same seed + same pool -> bit-identical offer in relic_id and order.", created: 2026-08-21, revised: "", status: active } + - { id: TR-reliquias-015, system: reliquias, gdd: design/gdd/reliquias-bendiciones.md, domain: Data, anchor: "F-RB3 / AC-RB19", requirement: "The pool is an ordered list of relic_id by ascending forge_index (deterministic order) and the draw is fisher_yates_partial(pool, k_effective, rng_seeded) without replacement, k_effective=min(draw_size, P).", created: 2026-08-21, revised: "", status: active } + - { id: TR-reliquias-016, system: reliquias, gdd: design/gdd/reliquias-bendiciones.md, domain: Data, anchor: "F-RB3 / AC-RB20", requirement: "P=0 invoked forcibly (bypassing the READY state gate) fails loudly — a signal of an upstream state-machine bug.", created: 2026-08-21, revised: "", status: active } + - { id: TR-reliquias-017, system: reliquias, gdd: design/gdd/reliquias-bendiciones.md, domain: Data, anchor: "F-RB3 Rule B / AC-RB22 / OQ-RB6", requirement: "No offer contains more than max_legendary_per_draw Legendaries; the exact algorithm (weighted sub-pool vs excess reroll) is implementation, and the AC requires zero violations over 1000 draws with fixed seeds [1..1000].", created: 2026-08-21, revised: "", status: active } + - { id: TR-reliquias-018, system: reliquias, gdd: design/gdd/reliquias-bendiciones.md, domain: Data, anchor: "F-RB5 / AC-RB37/RB38", requirement: "active_pool_size=min(total_relics_forged, pool_cap); on exceeding the cap, the lowest-forge_index relic changes its ephemeral 'in draw pool' flag to false — the RelicRecord is never deleted or touched.", created: 2026-08-21, revised: "", status: active } + - { id: TR-reliquias-019, system: reliquias, gdd: design/gdd/reliquias-bendiciones.md, domain: Data, anchor: "Edge Cases (pool_cap crossing intra-era)", requirement: "If a relic is retired from the pool while being offered in an open OFFERING, the in-progress offer is honored to resolution; the retirement applies to the next draw.", created: 2026-08-21, revised: "", status: active } + - { id: TR-reliquias-020, system: reliquias, gdd: design/gdd/reliquias-bendiciones.md, domain: Timing, anchor: "F-RB4 / AC-RB33", requirement: "Expiry is computed by physics tick: duration_ticks=ceil(duration_s*physics_ticks_per_second), expiry_tick=applied_tick+duration_ticks, is_active(t)=applied_tick<=tSPENT directly without passing through ACTIVE.", created: 2026-08-21, revised: "", status: active } + - { id: TR-reliquias-024, system: reliquias, gdd: design/gdd/reliquias-bendiciones.md, domain: Timing, anchor: "Regla 7b / OQ-RB9", requirement: "During OFFERING, the full simulation (kaiju, telegraph, units) slows uniformly to offering_time_scale (MVP 0.2x) rather than stopping; this system REQUESTS the scale but does NOT own the sim clock. (Landed by ADR-0002: requests SLOWED.)", created: 2026-08-21, revised: "2026-08-21", status: active } + - { id: TR-reliquias-025, system: reliquias, gdd: design/gdd/reliquias-bendiciones.md, domain: Timing, anchor: "OQ-RB9", requirement: "Who applies the Engine.time_scale-equivalent, how it interacts with F-RB4's fixed physics step (tick expiry must stay correct under time scale), and which systems are EXEMPT (e.g. the offering's own UI) must be fixed. (Landed by ADR-0002: TimeControl owns it.)", created: 2026-08-21, revised: "2026-08-21", status: active } + - { id: TR-reliquias-026, system: reliquias, gdd: design/gdd/reliquias-bendiciones.md, domain: Timing, anchor: "OQ-RB9 (death-pause interaction)", requirement: "Time-regime precedence: Permadeath's total pause (game_time_paused) WINS over offering_time_scale; during the death pause the offer is PRESERVED (not cancelled, soft-timeout not running) and resumes its slow-mo on pause end — requires adding Reliquias to Permadeath Regla 4 game_time_paused consumers. (Ratified by ADR-0002.)", created: 2026-08-21, revised: "2026-08-21", status: active } + - { id: TR-reliquias-027, system: reliquias, gdd: design/gdd/reliquias-bendiciones.md, domain: Timing, anchor: "Regla 7b / AC-RB15b", requirement: "Anti-stall brakes run in REAL time (not scaled): offering_soft_timeout_s (MVP 8.0s) auto-cancels the offer without spending a charge, and offering_cooldown_s (MVP 3.0s at normal speed) keeps can_invoke=false after any cancellation.", created: 2026-08-21, revised: "", status: active } + - { id: TR-reliquias-028, system: reliquias, gdd: design/gdd/reliquias-bendiciones.md, domain: Data, anchor: "Regla 7b / AC-RB15/RB15b", requirement: "Commit-on-choice: the charge decrements on the OFFERING->RESOLVING transition (on choosing), not on opening the offer; cancelling (manual or timeout) spends no charge.", created: 2026-08-21, revised: "", status: active } + - { id: TR-reliquias-029, system: reliquias, gdd: design/gdd/reliquias-bendiciones.md, domain: Data, anchor: "Regla 5 / AC-RB40", requirement: "The charge is spent on choosing even if the effect results in a no-op/whiff (e.g. rally with the band at its cap).", created: 2026-08-21, revised: "", status: active } + - { id: TR-reliquias-030, system: reliquias, gdd: design/gdd/reliquias-bendiciones.md, domain: Data, anchor: "Regla 5 / AC-RB17", requirement: "invocation_charges (and possibly draw_size, pool_cap) are authorable per era via EraDefinition; on era activation, invocation_charges_remaining is set to the new era's value without carrying over leftovers.", created: 2026-08-21, revised: "", status: active } + - { id: TR-reliquias-031, system: reliquias, gdd: design/gdd/reliquias-bendiciones.md, domain: Communication, anchor: "Regla 7 / AC-RB24/RB25", requirement: "The invocation window is gated by reading the Temporizador phase (CLIMAX); this system consumes the phase, never owns or re-derives it.", created: 2026-08-21, revised: "", status: active } + - { id: TR-reliquias-032, system: reliquias, gdd: design/gdd/reliquias-bendiciones.md, domain: Data, anchor: "Regla 10 / OQ-RB5", requirement: "The pool is populated exclusively with relics from PREVIOUS eras; an intra-era death forges its relic (Permadeath->Forja) but does not make it draftable in the same combat. For the single-era MVP, the pool must be PRE-SEEDED from a test pantheon of a fictional 'era 0'.", created: 2026-08-21, revised: "", status: active } + - { id: TR-reliquias-033, system: reliquias, gdd: design/gdd/reliquias-bendiciones.md, domain: Data, anchor: "Regla 9 / AC-RB28/RB29", requirement: "With an empty pool the READY state is never reached and there is no separate 'system blocked by progress' flag — the onboarding gate is structural, not a lock.", created: 2026-08-21, revised: "", status: active } + - { id: TR-reliquias-034, system: reliquias, gdd: design/gdd/reliquias-bendiciones.md, domain: Persistence, anchor: "OQ-RB2 / AC-RB46 / Edge Cases", requirement: "Ephemeral invocation state (spent charges + active modifiers with their remaining expiry tick) must restore on a mid-era reload without refunding spent charges; provisional round-trip contract with Guardado (save_ephemeral_invocation_state / load_ephemeral_invocation_state).", created: 2026-08-21, revised: "", status: active } + - { id: TR-reliquias-035, system: reliquias, gdd: design/gdd/reliquias-bendiciones.md, domain: Persistence, anchor: "OQ-RB2", requirement: "How a modifier's remaining time is recomputed across a reload must be defined (expiry_tick is relative to a session tick counter).", created: 2026-08-21, revised: "", status: active } + - { id: TR-reliquias-036, system: reliquias, gdd: design/gdd/reliquias-bendiciones.md, domain: Platform, anchor: "Dependencies (Input) / RU-2", requirement: "Bidirectional interface with Input: this system requests the BLESSING_SELECT context; Input emits blessing_choice_confirmed(index) / blessing_selection_cancelled that this system consumes.", created: 2026-08-21, revised: "", status: active } + - { id: TR-reliquias-037, system: reliquias, gdd: design/gdd/reliquias-bendiciones.md, domain: Communication, anchor: "RU-1..RU-6 / AC-U30", requirement: "Data surface exposed to UI/HUD (read-only): invocation_charges_remaining + era cap (RU-1), can_invoke (RU-2), single event first_invocation_available (at most once per save, RU-2b), blessing_offer (RU-3), band_headroom per rally option (RU-3b), active_blessings list with expiry ticks (RU-4), rejection event (RU-6).", created: 2026-08-21, revised: "", status: active } + - { id: TR-reliquias-038, system: reliquias, gdd: design/gdd/reliquias-bendiciones.md, domain: Data, anchor: "RU-3 / RV-2", requirement: "blessing_offer includes per option: relic_id, icon (from Forja, relic_category x tier), glanceable identity (source_hero_name + hero portrait/silhouette), relic_category, tier, and epitaph as a secondary channel.", created: 2026-08-21, revised: "", status: active } + - { id: TR-reliquias-039, system: reliquias, gdd: design/gdd/reliquias-bendiciones.md, domain: Data, anchor: "RU-2 / Regla 7b", requirement: "can_invoke is the conjunction: phase=CLIMAX AND charges>0 AND pool not empty AND not in post-cancellation cooldown.", created: 2026-08-21, revised: "", status: active } + - { id: TR-reliquias-040, system: reliquias, gdd: design/gdd/reliquias-bendiciones.md, domain: Data, anchor: "Edge Cases / AC-RB41", requirement: "An active modifier is discarded with the hero instance on entering DYING (does not survive its bearer or transfer) and does not alter Combate's D/T/P classification of the death.", created: 2026-08-21, revised: "", status: active } + - { id: TR-reliquias-041, system: reliquias, gdd: design/gdd/reliquias-bendiciones.md, domain: Platform, anchor: "RV-3", requirement: "Magnitude (tier) must read by enhancement intensity/duration, redundant with the icon — never by color alone (>=3 channels, consistent with UI/HUD HU-10).", created: 2026-08-21, revised: "", status: active } + + # ─── UI/HUD (Presentation) — design/gdd/ui-hud.md ─────────────────────────── + - { id: TR-ui-001, system: ui, gdd: design/gdd/ui-hud.md, domain: Ownership, anchor: "Core Rule 1 / AC-U01", requirement: "UI/HUD is purely read: it writes no state back to any consumed system, with the single exception of the Sacrifice visual feedback (provisional feedback-only contract, no command authority).", created: 2026-08-21, revised: "", status: active } + - { id: TR-ui-002, system: ui, gdd: design/gdd/ui-hud.md, domain: Ownership, anchor: "Overview / Interactions", requirement: "UI/HUD owns no game data of its own — it only reads events and state exposed by Combate, Heroes, Control, Temporizador, Kaiju, Reliquias, Input and Datos de Era.", created: 2026-08-21, revised: "", status: active } + - { id: TR-ui-003, system: ui, gdd: design/gdd/ui-hud.md, domain: Engine, anchor: "Core Rule 11 / AC-U02", requirement: "World-anchored overlays are implemented as Control nodes inside a CanvasLayer, positioned per-frame via F2.", created: 2026-08-21, revised: "", status: active } + - { id: TR-ui-004, system: ui, gdd: design/gdd/ui-hud.md, domain: Engine, anchor: "Core Rule 11 / AC-U02/U28", requirement: "Each overlay Control must set mouse_filter=MOUSE_FILTER_IGNORE; otherwise it would silently consume unit-selection clicks meant for the units below.", created: 2026-08-21, revised: "", status: active } + - { id: TR-ui-005, system: ui, gdd: design/gdd/ui-hud.md, domain: Platform, anchor: "Core Rule 11 / HU-9 / AC-U28", requirement: "No HUD element accepts direct input except the virtual gamepad cursor, whose behavior (magnetic snap) belongs to Control and of which UI/HUD only draws the appearance.", created: 2026-08-21, revised: "", status: active } + - { id: TR-ui-006, system: ui, gdd: design/gdd/ui-hud.md, domain: Engine, anchor: "F2 (MANDATORY impl note) / AC-U26", requirement: "F2 (world_to_screen_transform) is the mathematical projection definition, NOT the implementation path: the implementation must read the native canvas transform via CanvasItem.get_global_transform_with_canvas() (or the viewport's get_canvas_transform()) and Camera2D.get_screen_center_position() — NOT the node's raw .position.", created: 2026-08-21, revised: "", status: active } + - { id: TR-ui-007, system: ui, gdd: design/gdd/ui-hud.md, domain: Engine, anchor: "F2 (impl note) / camera_pan_speed=30", requirement: "Rationale for TR-ui-006: with position_smoothing_enabled or drag margins active, the rendered center lags the node position and a manual formula would make overlays SLIDE off their entities during panning; the native transform also absorbs content_scale_mode and DPI/resize scaling.", created: 2026-08-21, revised: "", status: active } + - { id: TR-ui-008, system: ui, gdd: design/gdd/ui-hud.md, domain: Engine, anchor: "F2 (impl note)", requirement: "The world uses Camera2D (pure 2D); F2 carries no rotation term because the camera does not rotate in MVP — adding rotation would break the manual F2.", created: 2026-08-21, revised: "", status: active } + - { id: TR-ui-009, system: ui, gdd: design/gdd/ui-hud.md, domain: Data, anchor: "F2 Output Range / Core Rule 6b(a) / AC-U26", requirement: "F2 output is unbounded — it may fall outside [0, viewport_size_px]; elements that must stay visible off-screen (attention-director directional marker) apply their own clamp/reprojection to the edge.", created: 2026-08-21, revised: "", status: active } + - { id: TR-ui-010, system: ui, gdd: design/gdd/ui-hud.md, domain: Data, anchor: "F1 / AC-U10/U12/U12b", requirement: "hero_risk_state=(danger_time_sFROZEN) / Edge Cases / AC-U16", requirement: "The readout freezes on the D/T/P snapshot on entering DYING EVEN IF nothing renders (hero off-screen / not selected); if the player brings it into view before DEAD, the frozen state renders immediately without re-triggering the freeze.", created: 2026-08-21, revised: "", status: active } + - { id: TR-ui-016, system: ui, gdd: design/gdd/ui-hud.md, domain: Communication, anchor: "States and Transitions", requirement: "FROZEN->HIDDEN triggers when Permadeath confirms DEAD — UI/HUD consumes that event, does not derive it.", created: 2026-08-21, revised: "", status: active } + - { id: TR-ui-017, system: ui, gdd: design/gdd/ui-hud.md, domain: Engine, anchor: "Core Rule 6b / HU-11 / AC-U32", requirement: "The attention director fires three simultaneous redundant channels when a hero crosses to AT_RISK/DYING off-screen or unselected: (a) edge-clamped directional marker via F2, (b) audio sting + haptic pulse once on the transition, (c) camera nudge event; all three cease when the hero enters the viewport or DEAD is confirmed.", created: 2026-08-21, revised: "", status: active } + - { id: TR-ui-018, system: ui, gdd: design/gdd/ui-hud.md, domain: Data, anchor: "F3 / AC-U22", requirement: "telegraph_vignette_intensity=clamp(base(third)+amplitude*abs(sin(pi*freq(third)*elapsed_in_third_s)), 0.0, 1.0); the clamp() is mandatory, not cosmetic — without it the output exceeds 1.0 at the top of the safe amplitude range (0.75+0.35=1.10).", created: 2026-08-21, revised: "", status: active } + - { id: TR-ui-019, system: ui, gdd: design/gdd/ui-hud.md, domain: Platform, anchor: "F3 / Core Rule 8 / AC-U19", requirement: "freq(third)=min(1.0+third, 3.0) Hz — a hard 3 flashes/sec photosensitivity cap, never exceeded.", created: 2026-08-21, revised: "", status: active } + - { id: TR-ui-020, system: ui, gdd: design/gdd/ui-hud.md, domain: Platform, anchor: "Core Rule 8 / Edge Cases / AC-U20/U23", requirement: "The reduced-flash toggle applies GLOBALLY to every flashing HUD element (F3 vignette AND the IMMINENT escalation), replacing the oscillating term with a monotone linear ramp; a per-element toggle would leave a photosensitivity gap.", created: 2026-08-21, revised: "", status: active } + - { id: TR-ui-021, system: ui, gdd: design/gdd/ui-hud.md, domain: Platform, anchor: "F3 Output Range / Example", requirement: "Both modes (flash and reduced-flash) share the same base() floor — that is what preserves accessibility parity.", created: 2026-08-21, revised: "", status: active } + - { id: TR-ui-022, system: ui, gdd: design/gdd/ui-hud.md, domain: Platform, anchor: "HU-10 / AC-U29", requirement: "Every escalation signal must be distinguishable with >=3 redundant channels (shape/frame + motion primary, color tertiary), verifiable under protanopia + deuteranopia + tritanopia simulation with 100% correct identification.", created: 2026-08-21, revised: "", status: active } + - { id: TR-ui-023, system: ui, gdd: design/gdd/ui-hud.md, domain: Data, anchor: "Interactions / Dependencies / F2", requirement: "UI/HUD consumes registered constants from Datos de Era (world_unit_scale=48, camera_pan_speed=30, zoom_min=0.5, zoom_max=1.5) along with the inverse screen_to_world_transform; it does not duplicate them.", created: 2026-08-21, revised: "", status: active } + - { id: TR-ui-024, system: ui, gdd: design/gdd/ui-hud.md, domain: Platform, anchor: "Dependencies (Input) / input Regla Nucleo 3", requirement: "UI/HUD consumes from Input: ui_* actions in menu/BLESSING_SELECT contexts, active_input_method (only for prompt icons, NOT focus-ring visibility), and input_lockout_active (HUD freeze during DEATH_HOLD).", created: 2026-08-21, revised: "", status: active } + - { id: TR-ui-025, system: ui, gdd: design/gdd/ui-hud.md, domain: Platform, anchor: "Dependencies (Input)", requirement: "The controls remapping screen is owned by UI/HUD and reads/writes the InputMap Input exposes — the only UI/HUD write surface beyond the Sacrifice feedback.", created: 2026-08-21, revised: "", status: active } + - { id: TR-ui-026, system: ui, gdd: design/gdd/ui-hud.md, domain: Timing, anchor: "Edge Cases / AC-U31 / OQ-U5", requirement: "When game_time_paused==true, all data-driven HUD animations freeze in place — per-channel verification: (a) health-bar drain, (b) ritual-timer countdown, (c) F3 telegraph vignette pulse. (Flag source now TimeControl per ADR-0002.)", created: 2026-08-21, revised: "2026-08-21", status: active } + - { id: TR-ui-027, system: ui, gdd: design/gdd/ui-hud.md, domain: Communication, anchor: "States (Timer ritual)", requirement: "UI/HUD consumes the entered_imminent / entered_climax signals from the Temporizador — it does not re-derive the phases.", created: 2026-08-21, revised: "", status: active } + - { id: TR-ui-028, system: ui, gdd: design/gdd/ui-hud.md, domain: Data, anchor: "F3 / Tuning Knobs", requirement: "UI/HUD consumes telegraph_progress [0.0,1.0] from Kaiju and derives third=floor(telegraph_progress*3) (clamped); telegraph_duration_s and guard_radius are consumed, not duplicated as own knobs.", created: 2026-08-21, revised: "", status: active } + - { id: TR-ui-029, system: ui, gdd: design/gdd/ui-hud.md, domain: Ownership, anchor: "Interactions (Kaiju) / Overview", requirement: "UI/HUD consumes a single discrete victory/defeat event from Kaiju; it does not own or render the result screen (only a brief acknowledgement before the hand-off).", created: 2026-08-21, revised: "", status: active } + - { id: TR-ui-030, system: ui, gdd: design/gdd/ui-hud.md, domain: Communication, anchor: "Interactions / RU-1..RU-4 / AC-U30", requirement: "UI/HUD consumes from Reliquias (read-only, no mutation): invocation_charges_remaining, can_invoke, first_invocation_available, blessing_offer 3-choose-1 with glanceable identity + band_headroom, and active_blessings list with expiry ticks.", created: 2026-08-21, revised: "", status: active } + - { id: TR-ui-031, system: ui, gdd: design/gdd/ui-hud.md, domain: Timing, anchor: "Interactions (cognitive load) / OQ-U8", requirement: "The invocation draft runs during CLIMAX in slow-mo (offering_time_scale), not paused — it must be counted as an additional active stream in the CLIMAX HUD cognitive-load analysis.", created: 2026-08-21, revised: "", status: active } + - { id: TR-ui-032, system: ui, gdd: design/gdd/ui-hud.md, domain: Performance, anchor: "Edge Cases (N simultaneous) / OQ-U8 / AC-U33", requirement: "Stacking of N simultaneous AT_RISK readouts lacks a prioritization/aggregation rule and a soft cap; N independent overlays have draw-call cost against the <1000 budget — an unresolved numeric performance risk.", created: 2026-08-21, revised: "", status: active } + - { id: TR-ui-033, system: ui, gdd: design/gdd/ui-hud.md, domain: Data, anchor: "Core Rule 10 / HU-8 / AC-U27", requirement: "The 'no editorializing text' verification is a deterministic sweep of all text elements against a constant list of forbidden phrases (exact comparison, no tester judgment) — a BLOCKING gate.", created: 2026-08-21, revised: "", status: active } + - { id: TR-ui-034, system: ui, gdd: design/gdd/ui-hud.md, domain: Engine, anchor: "Edge Cases (F2 outside viewport)", requirement: "A world-anchored element that F2 projects outside the viewport simply is not drawn that frame; no off-screen indicator is specified except the Core Rule 6b attention director.", created: 2026-08-21, revised: "", status: active } + - { id: TR-ui-035, system: ui, gdd: design/gdd/ui-hud.md, domain: Timing, anchor: "Tuning Knobs / Core Rule 7", requirement: "The Sacrifice feedback pulse duration is a real knob (sacrifice_feedback_pulse_s=0.15s), not 'one frame' — the framerate-dependent version is explicitly rejected.", created: 2026-08-21, revised: "", status: active } + - { id: TR-ui-036, system: ui, gdd: design/gdd/ui-hud.md, domain: Communication, anchor: "Core Rule 7 / AC-U17/U18 / OQ-U3", requirement: "The Sacrifice-order feedback requires an order_state in {IDLE, PENDING, CONFIRMED, REJECTED} state-machine contract from Combate/Control — currently only mocked, and the risk is one of SHAPE (if upstream lands an instant sacrifice with no confirmation, the mock is wrong, not just incomplete).", created: 2026-08-21, revised: "", status: active } + - { id: TR-ui-037, system: ui, gdd: design/gdd/ui-hud.md, domain: Data, anchor: "AC-U04", requirement: "Health-bar color is deterministically verifiable: sample the fill at 0%/50%/100% against the Bone->Old Blood gradient stops with tolerance deltaE<=2 (CIE76).", created: 2026-08-21, revised: "", status: active } + - { id: TR-ui-038, system: ui, gdd: design/gdd/ui-hud.md, domain: Platform, anchor: "Core Rule 4 / HU-2 / AC-U05", requirement: "Hero/troop distinction is by a shape notch in the outline, never by size or color alone, and must stay legible at the smallest sprite footprint (zoom_max=1.5 -> 32 px/unit).", created: 2026-08-21, revised: "", status: active } + - { id: TR-ui-039, system: ui, gdd: design/gdd/ui-hud.md, domain: Data, anchor: "F2 Example / Edge Cases", requirement: "Zoom convention identical to Datos de Era: zoom<1 zooms in (at zoom_min=0.5 -> 96 px/unit, sprites doubled); zoom_max=1.5 zooms out (32 px/unit, max unit density).", created: 2026-08-21, revised: "", status: active } + - { id: TR-ui-040, system: ui, gdd: design/gdd/ui-hud.md, domain: Ownership, anchor: "Overview / Core Rule 2 / AC-U02", requirement: "This GDD's scope is the active-session HUD overlay; menus (pause, settings, result, pantheon, era transition) are out of scope and there is no minimap in MVP.", created: 2026-08-21, revised: "", status: active } diff --git a/docs/architecture/traceability-index.md b/docs/architecture/traceability-index.md new file mode 100644 index 0000000000..1075be2ec6 --- /dev/null +++ b/docs/architecture/traceability-index.md @@ -0,0 +1,80 @@ +# Architecture Traceability Index + +**Last Updated:** 2026-08-21 +**Engine:** Godot 4.6 (pinned 2026-02-12) +**Source review:** `docs/architecture/architecture-review-2026-08-21.md` + +Full per-requirement IDs and text live in `docs/architecture/tr-registry.yaml` (399 entries). +This index records coverage status against the current ADR set. + +## Coverage Summary + +- Total requirements: **399** +- ✅ Covered: ~19 (~5%) +- ⚠️ Partial: ~8 +- ❌ Gaps: ~372 + +Coverage is early-stage by design: 2 ADRs exist against a 13-system corpus. The tracked +signal is not raw gap count but the **~7 architecture decisions the GDDs self-declare and +no ADR yet covers** (see Known Gaps). + +## Covered / Partial Matrix + +| TR-ID | GDD | Requirement (abridged) | ADR | Status | +|-------|-----|------------------------|-----|--------| +| TR-reliquias-003 | reliquias-bendiciones.md | `effective_stat(u,s,t)` per-instance over read-only base | ADR-0001 | ✅ | +| TR-reliquias-004 | reliquias-bendiciones.md | Combate exposes `effective_attack_damage(instance)` read-hook | ADR-0001 | ✅ | +| TR-reliquias-005 | reliquias-bendiciones.md | Modifier keyed `(relic_id, unit_instance_id)`, per-instance | ADR-0001 | ✅ | +| TR-reliquias-006 | reliquias-bendiciones.md | Never write `CombatProfile`/`HeroDefinition`; ephemeral layer only | ADR-0001 | ✅ | +| TR-reliquias-007 | reliquias-bendiciones.md | Additive across distinct relics; refresh-on-repick per relic_id | ADR-0001 | ✅ | +| TR-reliquias-008 | reliquias-bendiciones.md | `max_stat_bonus(s)` bound computable | ADR-0001 | ⚠️ (structure only) | +| TR-reliquias-009 | reliquias-bendiciones.md | Flat additive `+N`, no percent multipliers (Combate Regla 5) | ADR-0001 | ✅ | +| TR-reliquias-020 | reliquias-bendiciones.md | Expiry by physics tick, semi-open interval | ADR-0001 + ADR-0002 | ✅ (tick source = TimeControl.game_tick) | +| TR-reliquias-021 | reliquias-bendiciones.md | Expiry swept at tick start, before phase-1 damage | ADR-0001 | ✅ | +| TR-reliquias-022 | reliquias-bendiciones.md | Defensive expiry clamps, never `apply_damage`, never kills | ADR-0001 | ✅ | +| TR-reliquias-023 | reliquias-bendiciones.md | Instant effects skip tick bookkeeping | ADR-0001 | ⚠️ (partial) | +| TR-combate-001 | combate-dano.md | Single `apply_damage` path (Regla 1) | ADR-0001 | ✅ (constraint honoured) | +| TR-permadeath-003 | permadeath.md | `game_time_paused` ownership + consumers | ADR-0002 | ✅ (owner → TimeControl; Permadeath requests PAUSED) | +| TR-permadeath-004 | permadeath.md | Cooperative pause, never `SceneTree.paused` | ADR-0002 | ✅ | +| TR-permadeath-006 | permadeath.md | Beat clock exempt, unscaled delta | ADR-0002 | ⚠️ (mechanism reconciliation — see GDD flag) | +| TR-reliquias-024 | reliquias-bendiciones.md | `offering_time_scale` uniform slow-mo; requests, not owns | ADR-0002 | ✅ | +| TR-reliquias-025 | reliquias-bendiciones.md | Time-scale applier + fixed-step interaction + exemptions | ADR-0002 | ✅ | +| TR-reliquias-026 | reliquias-bendiciones.md | Precedence PAUSED > SLOWED; offering preserved | ADR-0002 | ✅ | +| TR-reliquias-027 | reliquias-bendiciones.md | Anti-stall timers run real-time | ADR-0002 | ⚠️ (exemption implied, not enumerated) | +| TR-combate-029 | combate-dano.md | Cooldowns follow game time; freeze on `game_time_paused` | ADR-0002 | ✅ | +| TR-kaiju-029 | encuentro-con-kaiju.md | Cooperative freeze on `game_time_paused` (Regla 11) | ADR-0002 | ✅ | +| TR-kaiju-030 | encuentro-con-kaiju.md | All clocks accumulate game-time delta in `_physics_process` | ADR-0002 | ⚠️ (clock source now TimeControl) | +| TR-temporizador-010 | temporizador-de-preparacion-ritual.md | `elapsed_s` accumulates game-time delta (pause/scale aware) | ADR-0002 | ✅ | +| TR-heroes-023 | sistema-de-heroes.md | Beat animation node exempt from pause | ADR-0002 | ⚠️ (mechanism reconciliation — see GDD flag) | + +## Known Gaps (require ADRs) + +Foundation layer: +- **Save / Persistence** — checksum, serialization, ephemeral-invocation round-trip + (OQ-RB2), `expiry_tick` rebasing on reload, `forge_counter` int64, schema migration. + Cross-review blocker S4; both existing ADRs defer to it. +- **Era lifecycle / init-ordering** — `LOADING→LOADED`, era-`LOADED` signal, Autoload + init order (TR-era-011, TR-permadeath-017), per-era cap re-validation. +- **Camera/zoom constant ownership** — `world_unit_scale`/`camera_pan_speed`/`zoom_min`/ + `zoom_max` (Era vs Control), `Camera2D.zoom` sign (OQ-INPUT-2, F2-W5). +- **Input context enforcement** — `current_context` contract (OQ-INPUT-5) + Pillar-1 + frame ordering Input→Control→Permadeath (OQ-INPUT-1). + +Feature layer: +- **`CombatProfile` storage** — Combate OQ-1 (embedded vs parallel resource); ADR-0001 + left it open. +- **Relic-registry representation** — Forja OQ-FL4/FL5 + Reliquias OQ-RB6. +- **Kaiju intent-controller** — controller home + immediate `apply_damage` wiring + sole- + caller exclusion from Combate's auto-attack loop. +- **Permadeath implementation-pattern** — OQ-P5 (Autoload vs node, reset_queue softlock, + hook forms, 4.6 engine spike). + +Cross-cutting: +- **Config-enforcement + determinism convention** — loud-fail vs clamp split across ~10 + GDDs; seeded-RNG / monotonic-`entity_id` seam. ADR or control-manifest. + +## Superseded Requirements + +None yet — first review; no GDD was revised after an ADR referencing it was written. +(Note: ADR-0002 supersedes ADR-0001's provisional `Engine.get_physics_frames()` tick +source with `TimeControl.game_tick` — an ADR-internal supersession, not a requirement one.) diff --git a/docs/consistency-report-2026-08-06.md b/docs/consistency-report-2026-08-06.md new file mode 100644 index 0000000000..bdf9f914d6 --- /dev/null +++ b/docs/consistency-report-2026-08-06.md @@ -0,0 +1,50 @@ +# Consistency Check Report +Date: 2026-08-06 +Registry entries checked: 3 entities, 0 items, 3 formulas, 5 constants (11 total) +GDDs scanned: 6 (datos-de-era-civilizacion.md, guardado-persistencia.md, input.md, control-y-seleccion-de-unidades.md, sistema-de-tropas.md, sistema-de-heroes.md) + +--- + +### Conflicts Found (must resolve before architecture) + +None. + +--- + +### Stale Registry Entries (registry behind the GDD) + +None. + +--- + +### Unverifiable References (no conflict, informational) + +None — every registered entity/formula/constant referenced across GDDs stated a comparable value, and all values matched the registry. + +--- + +### Clean Entries (no issues found) + +✅ **Entities (3/3 clean)** +- `melee_infantry` — max_health=40 confirmed identical in `sistema-de-tropas.md` (source) and referenced correctly as `melee_troop_max_health=40` in `sistema-de-heroes.md` (Formula H1 anchor). +- `ranged_troop`, `support_troop` — only referenced in their source GDD (`sistema-de-tropas.md`); no cross-doc mentions found, consistent with registry's `referenced_by`. + +✅ **Formulas (3/3 clean)** +- `screen_to_world_transform` — expression, variables (`screen_pos_px`, `viewport_size_px`, `camera_position_world`, `zoom`, `world_unit_scale`) match `control-y-seleccion-de-unidades.md` Fórmula 2 exactly. +- `select_radius` — expression, variables, and output_range `[0.35, 1.2]` match `control-y-seleccion-de-unidades.md` Fórmula 1 exactly. +- `relic_quality` — expression, variables (D, T, P, base_accidental, base_deliberate, w_stakes, w_purpose), and output_range `[0.10, 1.00]` match `sistema-de-heroes.md` Formula H4 exactly (registered this session). + +✅ **Constants (5/5 clean)** +- `world_unit_scale`=48 — consistent across datos-de-era-civilizacion.md, input.md, control-y-seleccion-de-unidades.md, sistema-de-tropas.md, sistema-de-heroes.md. +- `camera_pan_speed`=30 — consistent across the same 5 documents (incl. `effective_pan_speed_world` derivation in control-y-seleccion-de-unidades.md, which correctly treats 30 as the zoom-1.0 reference value, not a redefinition). +- `zoom_min`=0.5, `zoom_max`=1.5 — consistent across datos-de-era-civilizacion.md, input.md, control-y-seleccion-de-unidades.md. +- `reinforcement_ratio`=0.25 (safe range 0.15–0.35) — consistent in sistema-de-tropas.md (source), matches registry's tuning note. + +**Cross-check specific to this session's new GDD (Sistema de Héroes):** +- Hero `unit_visual_radius_world` range [0.5, 1.0] stays strictly above the troop ceiling (0.4583 = `melee_infantry`), preserving hero > troop selection priority — confirmed consistent between `sistema-de-tropas.md` AC-12 and `sistema-de-heroes.md` AC-H05. +- Hero `move_speed` range (3.5–4.25 u/s) stays strictly below the fastest troop (`ranged_troop`=5.0 u/s) — confirmed consistent between `sistema-de-tropas.md` AC-13 and `sistema-de-heroes.md` Formula H2 example. +- The 3 hero archetypes (Vanguardia/Portaestandarte/Guardián) were correctly **not** registered as entities — they are illustrative of the 3 `relic_category` values, not a closed MVP roster (per OQ-5 in sistema-de-heroes.md). No conflict; noted as an intentional scope decision. + +--- + +Verdict: **PASS** diff --git a/docs/consistency-report-2026-08-12.md b/docs/consistency-report-2026-08-12.md new file mode 100644 index 0000000000..df0721b385 --- /dev/null +++ b/docs/consistency-report-2026-08-12.md @@ -0,0 +1,64 @@ +# Consistency Check Report — 2026-08-12 + +**Trigger:** Post-approval of `encuentro-con-kaiju.md` (system #9). +**Registry checked:** 4 entities, 0 items, 5 formulas, 6 constants. +**GDDs scanned (9):** input, control-y-seleccion-de-unidades, sistema-de-heroes, +datos-de-era-civilizacion, guardado-persistencia, temporizador-de-preparacion-ritual, +sistema-de-tropas, combate-dano, encuentro-con-kaiju. + +## Verdict: PASS (0 value conflicts) + +### 🔴 Conflicts — cross-GDD value contradictions +**None.** Every registered value Encuentro con Kaiju touches agrees with its source: +- ✅ `kaiju` CombatProfile `45 / 2.5 / 4.5 / 10.0` — matches `combate-dano.md:89` and registry; `leash_range` nulled per Encuentro Regla 2 (consistent with registry note). +- ✅ `telegraph_duration_s` `14.0`, range `[12.5,17.0]`, hard min `12.0` — single-source, self-consistent. +- ✅ `time_to_death` formula + output range `[18.5, null]` — Encuentro (owner) and Héroes (consumer) agree. +- ✅ `melee_infantry` DPS `8` (=8/1.0) — matches Encuentro F4 sanity-check. +- ✅ `guard_radius` `4.0` — Héroes (owner), Encuentro (KV-2), Combate (AC-C32) all agree. + +### ⚠️ Stale Registry (1) — ✅ APPLIED +- `kaiju_timer_remaining_ratio.referenced_by` now includes `encuentro-con-kaiju.md` + (read-only consumer, AC-K04); stale "sin GDD aún" comment removed; `revised: 2026-08-12`. + +### Registry Gaps (2) — ✅ APPLIED +- `guard_radius` (constant, `4.0`, safe range 2.5–6.0) — registered; source Héroes, referenced_by Héroes + Encuentro + Combate. +- `incoming_damage_rate_total` (formula) — registered; source Encuentro F6, referenced_by Encuentro + Héroes. + +### Internal Fix Applied +- `encuentro-con-kaiju.md:333` read `min_troop_dps` while the F4 box + AC-K85 use + `max_single_troop_dps` (a loose end from the round-3 F4 enforcement edit). Corrected. + +## OQ-4 Cross-GDD Cleanup — APPLIED (de-flag + re-verify blocked mocks) + +Encuentro con Kaiju is Approved, so four GDDs' "sin GDD / Provisional" references were +de-flagged and their blocked ACs re-verified against Encuentro's real contract: + +**Combate/Daño** (Approved) +- Interactions + Dependencies rows → Approved reference. +- AC-C32/C33/C34 (`P`-side) → **desbloqueados** — Encuentro Regla 5/AC-K43/K44 define marking; verifiable without mock. +- AC-C35 → **partial** — Kaiju side resolved; D-side still blocked on Control's Sacrificio verb (OQ-2). +- OQ-5 (~30s window) → resolved by Encuentro F2. OQ-7 (kaiju leash) → resolved by Regla 2. OQ-8 (victory/defeat owner) → resolved by Regla 9. +- Line-91 leash rationale updated. + +**Temporizador de Preparación/Ritual** (Approved) +- Interactions + Dependencies rows → Approved reference. +- AC-T03b (consume-not-own) → **desbloqueado** — confirmed by Encuentro AC-K04 (read-only, never writes back). +- AC-T07b → **partial** — Kaiju side resolved; UI/Audio still sin GDD. +- OQ-2 (victory/defeat) → resolved by Encuentro Regla 9. OQ-4 (climax internal timer / enrage) → resolved by Regla 10. OQ-5 (consume-not-own) → resolved by AC-K04. + +**Sistema de Héroes** (Approved) +- Interactions row → Approved; notes `time_to_death` (F2) + `incoming_damage_rate_total` (F6), readout reflects most-dangerous source (AC-K68). +- AC-H29 → data contract resolved (Encuentro F2/F6); remains Integration pending playable era + Control's Sacrificio verb. +- AC-H30 → design contracts Approved; remains Integration pending an implemented era (vertical slice). +- AC-H32b → data contract resolved; concrete threshold value still open (OQ-6 / Encuentro OQ-14). +- Out-of-scope (left as-is): AC-H11/H21/H25/H34 reference Permadeath / Control Sacrificio / Audio — not Encuentro. + +**Sistema de Tropas** (Designed) +- Victory/defeat owner OQ → resolved (Encuentro Regla 9/AC-K53: troops are NOT a defeat condition — unblocks Tropas AC-25). +- Reinforcement-reset OQ + "troops expose positions" OQ → updated (Encuentro/Temporizador now Approved; targeting covered by Regla 3). + +**Encuentro con Kaiju** — its own OQ-4 marked ✅ Resolved. + +## Remaining Open (not actioned) +- OQ-10 — formalize a lock / re-verify trigger for the kaiju's `attack_damage`/`attack_cooldown_s` in `combate-dano.md` (belongs in that doc; AC-H29 depends on those values via Encuentro F2). +- `sistema-de-heroes.md:379` AC-H21 says "Combate/Daño, sin GDD" — factually stale (Combate is Approved; real blocker is Control's Sacrificio verb). Minor pre-existing wording issue, outside this OQ-4 pass. diff --git a/docs/consistency-report-2026-08-14.md b/docs/consistency-report-2026-08-14.md new file mode 100644 index 0000000000..55297308df --- /dev/null +++ b/docs/consistency-report-2026-08-14.md @@ -0,0 +1,38 @@ +# Consistency Check Report — 2026-08-14 + +**Trigger**: tras completar `/design-system UI/HUD` (GDD #13 → Designed). +**Registry checked**: 4 entities, 0 items, 8 formulas, 7 constants +**GDDs scanned (11)**: input, control-y-seleccion-de-unidades, datos-de-era-civilizacion, guardado-persistencia, combate-dano, temporizador-de-preparacion-ritual, sistema-de-tropas, encuentro-con-kaiju, sistema-de-heroes, ui-hud, permadeath + +**Método**: grep-first. Superficie de riesgo enfocada en los 2 GDDs nuevos desde el último check (ui-hud.md, permadeath.md) y las 2 fórmulas nuevas (`hero_risk_threshold`, `world_to_screen_transform`). Baseline previo (2026-08-12, 9 GDDs, 0 conflictos) intacto: desde entonces solo se añadieron líneas `referenced_by` y una fila de dependencia en sistema-de-heroes.md — ningún valor registrado cambió. + +--- + +## 🔴 Conflicts Found + +Ninguno. + +## ⚠️ Stale Registry + +Ninguna. Todos los valores registrados coinciden con sus GDDs dueños. + +## ℹ️ Informational — contratos cross-GDD (no son conflictos de valor de registro) + +Fuera de la definición de conflicto de este skill; ya auto-flageados en las Open Questions de permadeath.md; se resolverán en su propio `/design-review`. + +- **`game_time_paused`**: contrato nuevo declarado por permadeath.md, aún no presente en el GDD Approved de Encuentro con Kaiju. UI/HUD (AC-U31 / OQ-U5) y Permadeath (AC-P14) lo tratan consistentemente como contrato provisional — concuerdan, sin contradicción. +- **Héroes AC-H12 payload**: permadeath.md (OQ-P6) requiere que Héroes incluya `relic_quality` en el payload de `DYING`; hoy AC-H12 expone solo `hero_id`/`hero_name`/`relic_category`/D/T/P. Brecha de completitud de interfaz, no contradicción de valor. Requiere editar Héroes (Approved) en una revisión futura. + +## ✅ Clean Entries + +- **8 fórmulas**: `hero_risk_threshold` y `world_to_screen_transform` (nuevas, auto-contenidas en ui-hud.md); `screen_to_world_transform`, `select_radius`, `relic_quality`, `kaiju_timer_remaining_ratio`, `time_to_death`, `incoming_damage_rate_total` (existentes, consumidas con valores consistentes). +- **7 constantes**: `world_unit_scale`=48, `camera_pan_speed`=30, `zoom_min`=0.5, `zoom_max`=1.5, `reinforcement_ratio`=0.25, `telegraph_duration_s`=14.0, `guard_radius`=4.0 — todas coinciden en cada GDD que las cita. Verificación específica en ui-hud.md: `guard_radius`=4.0 ✅, `telegraph_duration_s`=14.0 ✅, `world_unit_scale`=48 ✅, `camera_pan_speed`=30 ✅, `zoom_min/max`=0.5/1.5 ✅. +- **4 entidades**: `melee_infantry`, `ranged_troop`, `support_troop`, `kaiju` — sin restatement de stats en los GDDs nuevos (ui-hud.md y permadeath.md no reafirman valores de combate). + +--- + +## Verdict: PASS + +Registro y GDDs concuerdan en todos los valores. Sin correcciones de registro necesarias. Sin entradas nuevas pendientes (las 2 fórmulas nuevas ya se registraron en la Fase 5 de UI/HUD). + +**Recomendado a continuación**: `/design-review design/gdd/ui-hud.md` en sesión limpia. Los 2 ítems informativos (game_time_paused, AC-H12 payload) se atenderán al revisar/aprobar permadeath.md. diff --git a/docs/consistency-report-2026-08-16.md b/docs/consistency-report-2026-08-16.md new file mode 100644 index 0000000000..c70a7b7edf --- /dev/null +++ b/docs/consistency-report-2026-08-16.md @@ -0,0 +1,46 @@ +# Consistency Check Report + +**Date**: 2026-08-16 +**Registry entries checked**: 4 entities, 1 item, 8 formulas, 7 constants +**GDDs scanned**: 12 (input, datos-de-era-civilizacion, guardado-persistencia, control-y-seleccion-de-unidades, sistema-de-tropas, sistema-de-heroes, temporizador-de-preparacion-ritual, combate-dano, encuentro-con-kaiju, permadeath, ui-hud, forja-de-legado) +**Scope**: full, con foco en los 7 documentos tocados desde el último check (2026-08-14): permadeath.md, sistema-de-heroes.md, guardado-persistencia.md, encuentro-con-kaiju.md, datos-de-era-civilizacion.md (todos editados en la reconciliación cross-GDD OQ-P6), y forja-de-legado.md (nuevo). + +--- + +## Conflicts Found (must resolve before architecture) + +Ninguno. 🎉 + +--- + +## Stale Registry Entries (registry behind the GDD) + +Ninguno. El registro se actualizó en la misma sesión que los GDDs: +- `relic_quality.referenced_by` += forja-de-legado.md (revised 2026-08-16). +- `relic_record` (nuevo item) registrado con source forja-de-legado.md. + +--- + +## Unverifiable References (no conflict, informational) + +- `game_time_paused` (flag introducido por Permadeath Regla 4, ahora consumido por Encuentro con Kaiju Regla 11/AC-K89 y Combate/Daño AC-C52) **no está registrado en entities.yaml** — es un flag booleano de runtime, no una constante/fórmula/entidad con valor numérico. Documentado en los GDDs, no en el YAML (coherente con la nota del check 2026-08-14). Sin conflicto; solo se anota. +- `relic_id` / `forge_index` / `forge_counter` (nuevos, de Forja) son campos internos de `relic_record` — registrados como parte de ese item, no como constantes independientes. Sin valor numérico fijo que pueda conflictuar. + +--- + +## Clean Entries (no issues found) + +✅ **Cortes de tier de reliquia** — Delgada `[0.10,0.35)` / Sólida `[0.35,0.70)` / Legendaria `[0.70,1.00]`: idénticos en `relic_quality` (registro, source sistema-de-heroes.md), Héroes AC-H19/H20, UI/HUD (U-2), y Forja de Legado FL1/AC-FL03..FL06. La regla explícita de Forja "no duplicar los cortes como knob" (Tuning Knobs) preserva la fuente de verdad única. +✅ **`relic_category`** — {offensive, defensive, rally}: consistente en Héroes (tabla de arquetipos: Vanguardia=offensive, Portaestandarte=rally, Guardián=defensive), Combate/Daño (misma tabla, etiquetas ofensivo/rally/defensivo), y Forja (transporta sin reasignar, AC-FL23 loud-fail ante categoría desconocida). +✅ **`relic_quality`** — output_range [0.10, 1.00], expresión y variables sin cambios; Forja lo consume/transporta sin recalcular (AC-FL07/AC-FL02 de Permadeath). El nuevo consumidor (Forja) no altera la fórmula. +✅ **`relic_record`** (nuevo item) — 9 campos; referencias bidireccionales verificadas: Datos de Era `legacy_props` → `relic_id` (Datos de Era Regla 4), Guardado persiste por referencia (Guardado Regla 4). Fuente de verdad canónica = Forja. +✅ **Constantes** (`world_unit_scale`=48, `camera_pan_speed`=30, `zoom_min`=0.5, `zoom_max`=1.5, `reinforcement_ratio`=0.25, `telegraph_duration_s`=14.0, `guard_radius`=4.0): ningún GDD editado esta sesión restató un valor distinto. Las ediciones de Kaiju (Regla 11 + AC-K89) y Héroes (AC-H12/H26/H27c) fueron aditivas, sin tocar valores. +✅ **Fórmulas cross-system** (`kaiju_timer_remaining_ratio`, `time_to_death`, `incoming_damage_rate_total`, `hero_risk_threshold`, transformadas de cámara): sin cambios; los documentos editados no las redefinen. + +--- + +**Verdict: PASS** — el registro y los 12 GDDs coinciden en todos los valores verificados. Cero conflictos. + +Notas de reconciliación pendiente (NO son conflictos de valor — son gaps estructurales de dependencia bidireccional, ya flageados en sus GDDs como Open Questions): +- OQ-FL1: Datos de Era/Civilización aún no lista a Forja en su tabla de Dependientes (a añadir al pasar Forja a Approved). +- `systems-index.md` fila 11 ya actualizada esta sesión con las 3 dependencias de Forja. diff --git a/docs/consistency-report-2026-08-18.md b/docs/consistency-report-2026-08-18.md new file mode 100644 index 0000000000..0a94e65fe0 --- /dev/null +++ b/docs/consistency-report-2026-08-18.md @@ -0,0 +1,51 @@ +# Consistency Check Report + +**Date:** 2026-08-18 +**Registry entries checked:** 4 entities, 1 item, 10 formulas, 10 constants +**Scope:** Focused on the 2026-08-18 design-review revision of `reliquias-bendiciones.md` (stacking model, cámara-lenta knobs, cross-category anchoring) against all 13 system GDDs +**GDDs scanned:** input, datos-de-era-civilizacion, guardado-persistencia, control-y-seleccion-de-unidades, sistema-de-tropas, sistema-de-heroes, temporizador-de-preparacion-ritual, combate-dano, encuentro-con-kaiju, permadeath, forja-de-legado, reliquias-bendiciones, ui-hud + +--- + +## Conflicts Found (must resolve before architecture) + +**None.** 🔴 0 conflicts. + +--- + +## Stale Registry Entries (registry behind the GDD) + +**None.** The `effective_attack_damage` note was re-synced to the new refresh-on-repick stacking rule during today's revision (registry `last_updated` = 2026-08-18). All other Reliquias-owned registry values were unchanged by the revision. + +--- + +## Verified Clean (Reliquias-owned values cross-checked post-revision) + +- ✅ **`invocation_charges` = 3** — registry matches GDD Tuning Knobs; no other GDD states a conflicting value (Datos de Era/UI-HUD listed as `posibles referenciadores`, no value stated). +- ✅ **`draw_size` = 3** — registry matches GDD; consistent across F-RB3, ACs, knobs. +- ✅ **`pool_cap` = 16** — registry matches GDD F-RB5 / knobs / AC-RB37/38. +- ✅ **`blessing_magnitude`** (offensive 8/12/18, defensive 30/45/68, rally 2/3/5) — unchanged by revision; registry note matches GDD table. +- ✅ **`effective_attack_damage`** — registry note re-synced to `(relic_id, unit)` refresh-on-repick (AC-RB31/RB31b); matches GDD F-RB1. Combate read-hook still provisional (OQ-RB1), correctly flagged in both docs — not a conflict. +- ✅ **`relic_record`** — Reliquias reads `relic_id, relic_category, tier, source_hero_name, epitaph, forge_index`; registry `fields` list matches; read-only (Regla 1) consistent with Forja ownership. + +## Anchor entities the new cross-category rules depend on (verified against owners) + +- ✅ **`melee_infantry.attack_damage` = 8** (registry, owned Combate) — Reliquias `offensive_damage_base=8` anchored to it. Consistent. +- ✅ **`kaiju.attack_damage` = 45** (registry + `combate-dano.md` line 150) — Reliquias defensive anchor "Sólida (45) = exactly one kaiju hit". Consistent. +- ✅ **Vanguardia `attack_damage` = 25** (`combate-dano.md` lines 86/148, owner) — Reliquias offensive ceiling anchor "25+18=43 < kaiju 45". Consistent. *(The héroes-GDD `3.5× / 140` for Vanguardia is `hero_max_health`, a different stat — no conflict.)* + +--- + +## Informational Notes (no conflict, no blocking action) + +ℹ️ **Hero CombatProfiles are not in the entity registry.** `Vanguardia.attack_damage=25` is a genuine cross-system fact (owned by `combate-dano.md`, consumed by `reliquias-bendiciones.md` for balance anchors, referenced by `sistema-de-heroes.md`), but no hero entity is registered. Registering it is arguably premature: `sistema-de-heroes.md` (line 496) flags Vanguardia/Portaestandarte/Guardián as *illustrative archetypes*, with the real roster deferred to Datos de Era/Civilización. **Recommendation:** register hero CombatProfiles when the first-era roster is fixed, not before — until then the values live authoritatively in `combate-dano.md`. + +ℹ️ **`offering_time_scale` × fixed physics tick is a design OQ, not a value conflict.** The new cámara-lenta knob interacts with F-RB4's tick-based expiration (`physics_ticks_per_second`, MVP 60) and Combate's fixed-tick two-phase damage order (Combate Core Rule 1). The models are consistent (Reliquias checks expiry *before* Combate's phase-1, as Combate's order requires), but *how* a global time-scale composes with the fixed step is correctly captured as **OQ-RB9** (to be resolved in the modifier-pattern ADR). No numeric conflict. + +ℹ️ **New Reliquias knobs are system-internal, unregistered by design.** `offering_time_scale`, `offering_soft_timeout_s`, `offering_cooldown_s` are referenced by no other GDD — per registry rules (only cross-boundary facts are registered), they are correctly left out. + +--- + +## Verdict: PASS + +No cross-document value conflicts. The 2026-08-18 Reliquias revision introduced no registry inconsistencies. Two design-level interactions (hero-CombatProfile registration timing; slow-mo × fixed tick) are correctly routed as deferred/OQ items, not conflicts. Safe to proceed to `/review-all-gdds`. diff --git a/docs/registry/architecture.yaml b/docs/registry/architecture.yaml index edfb05c78e..81c909d54b 100644 --- a/docs/registry/architecture.yaml +++ b/docs/registry/architecture.yaml @@ -30,7 +30,7 @@ # Superseded entries: Grep pattern="status: superseded" version: 1 -last_updated: "" +last_updated: "2026-08-21" # ADR-0004 (Era lifecycle/init-ordering, Proposed) +1 state_ownership (era_lifecycle_state), +1 interface (era_lifecycle), +2 forbidden_patterns (reading_eradefinition_in_ready, era_lifecycle_driven_outside_eramanager). Prev 2026-08-21: ADR-0003 (Save/Persistencia, Proposed) +1 state_ownership (persisted_game_state), +1 interface (persistence_contract), +3 forbidden_patterns (json_for_int64_persistence, persisting_absolute_game_tick, deserializing_objects_from_save), +2 api_decisions (save_serialization store_var/get_var; save_corruption_checksum hash()). Prev 2026-08-21: ADR-0001 y ADR-0002 movidos Proposed->Accepted tras /architecture-review; cleanups aplicados (ADR-0001 tick source -> TimeControl.game_tick; ADR-0002 process_physics_priority=-1000 + PROCESS_MODE_ALWAYS). Prev 2026-08-18: ADR-0002 (TimeControl) +1 state_ownership (game_time_regime), +1 interface (time_regime), +2 forbidden_patterns; ADR-0001 instance_combat_stats.referenced_by += ADR-0002. Prev: ADR-0001 +1 state_ownership +1 interface +2 forbidden_patterns # ─── STATE OWNERSHIP ───────────────────────────────────────────────────────── # Who is the authoritative owner of each piece of shared game state. @@ -43,7 +43,55 @@ last_updated: "" # 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: instance_combat_stats + status: active + owner_system: combate-dano + adr: docs/architecture/adr-0001-combat-stat-modifier-layer.md + interface: "CombatState.effective(stat) / effective_attack_damage() -> int (read-only pull, on-read over per-type CombatProfile + Array[StatModifier]). Base CombatProfile is a per-type Resource, read-only, never mutated at runtime." + write_access: combate-dano-only # only Combate applies/expires modifiers and computes effective stats; sources call add_modifier + referenced_by: + - docs/architecture/adr-0001-combat-stat-modifier-layer.md + - docs/architecture/adr-0002-time-control-service.md # expiry_tick / sweep_expired now read TimeControl.game_tick (supersedes provisional Engine.get_physics_frames()) + added: 2026-08-18 + revised: "2026-08-18" # ADR-0002: tick source for expiry is TimeControl.game_tick (pause-aware/scale-aware), not Engine.get_physics_frames() + + - state: game_time_regime + status: active + owner_system: time-control + adr: docs/architecture/adr-0002-time-control-service.md + interface: "TimeControl.time_scale (float, read-only), TimeControl.game_time_paused (bool, read-only; same semantics as Permadeath R4 game_time_paused), TimeControl.game_tick (int, monotonic game-time counter; advances scaled, freezes on pause). Regime set via request_regime(source_id, regime)/release_regime(source_id); effective = max precedence PAUSED>SLOWED>NORMAL." + write_access: time-control-only # only TimeControl mutates the regime; sources request/release, everyone else reads + referenced_by: + - docs/architecture/adr-0002-time-control-service.md + - docs/architecture/adr-0001-combat-stat-modifier-layer.md # blessing expiry reads game_tick + added: 2026-08-18 + revised: "" + + - state: persisted_game_state + status: active + owner_system: save-persistence + adr: docs/architecture/adr-0003-save-persistence-serialization.md + interface: "SaveService owns the on-disk save file, the versioned envelope {payload:{schema_version, sistemas:{...}}, checksum:int}, and the serialization mechanism. Each persistable system implements a symmetric serialize_state()->Dictionary / deserialize_state(Dictionary)->void; SaveService assembles sub-dicts under a per-system key without inspecting their meaning. Read/write via request_save()->bool / request_load()->LoadResult. Format: FileAccess.store_var/get_var (binary, native int64). No system persists TimeControl.game_tick or an absolute expiry_tick — only remaining_ticks (relative), rebased on load." + write_access: save-persistence-only # only SaveService writes/reads the file; systems supply/receive plain-Variant dicts + referenced_by: + - docs/architecture/adr-0003-save-persistence-serialization.md + - docs/architecture/adr-0001-combat-stat-modifier-layer.md # StatModifier persistence (remaining_ticks) resolved here + - docs/architecture/adr-0002-time-control-service.md # expiry_tick rebasing defined against game_tick, resolved here + added: 2026-08-21 + revised: "" + + - state: era_lifecycle_state + status: active + owner_system: era-lifecycle + adr: docs/architecture/adr-0004-era-lifecycle-init-ordering.md + interface: "EraManager owns the era state machine (UNLOADED/LOADING/LOADED/ACTIVE/ARCHIVED/FAILED) and the active EraDefinition. Drive via load_era(era_id)/activate_era()/archive_active_era(); read the active era via get_active_era() -> EraDefinition (null if none ACTIVE) or by handling era_loaded(era). Signals: era_loaded(era) [only after the validation gate passes], era_activated(era), era_archived(era), era_load_failed(era_id, reasons). EraDefinition stays a read-only Resource; EraManager never mutates it. Exactly one era ACTIVE at a time; ARCHIVED->ACTIVE forbidden without a fresh LOADING->LOADED cycle." + write_access: era-lifecycle-only # only EraManager drives transitions / loads eras; consumers read via signal or get_active_era() + referenced_by: + - docs/architecture/adr-0004-era-lifecycle-init-ordering.md + - docs/architecture/adr-0003-save-persistence-serialization.md # boot: SaveService.request_load() supplies era_id; per-era persisted state applied on era_loaded + added: 2026-08-21 + revised: "" # Example: # @@ -71,7 +119,75 @@ state_ownership: [] # # pattern options: signal | direct_call | event_bus | shared_resource | rpc | none -interfaces: [] +interfaces: + - contract: combat_stat_modifiers + status: active + pattern: direct_call # sources call Combate's API directly; Combate reads effective_* internally + producer: combate-dano # owns CombatState, the modifier list, application, and expiry + consumers: + - reliquias-bendiciones # writes flat additive modifiers; never reads/writes CombatProfile + adr: docs/architecture/adr-0001-combat-stat-modifier-layer.md + signal_signature: "add_modifier(mod: StatModifier) / remove_by_source(source_relic_id: String) / sweep_expired(current_tick: int); read: effective_attack_damage() -> int. StatModifier = {stat, magnitude:int>=1, expiry_tick:int, source_relic_id:String} (RefCounted). Dedup: one active modifier per (source_relic_id, stat) per unit — refresh-on-repick." + referenced_by: + - docs/architecture/adr-0001-combat-stat-modifier-layer.md + added: 2026-08-18 + revised: "" + + - contract: time_regime + status: active + pattern: shared_service # readers poll TimeControl; regime sources use a request/release API + producer: time-control # owns the game-time regime and the game_tick counter + consumers: + - permadeath # request_regime(PAUSED) on DEATH_HOLD; beat clock is exempt (real-time) + - reliquias-bendiciones # request_regime(SLOWED) with offering_time_scale while OFFERING open + - combate-dano # reads game_time_paused (AC-C52); reads game_tick for blessing expiry sweep + - encuentro-con-kaiju # reads game_time_paused (Regla 11 / AC-K89) + - input # reads game_time_paused to suppress input + - temporizador-de-preparacion-ritual # reads game_tick/time_scale (closes OQ-P4/F2-W4) + adr: docs/architecture/adr-0002-time-control-service.md + signal_signature: "request_regime(source_id: StringName, regime: TimeControl.Regime) / release_regime(source_id: StringName); reads: time_scale: float, game_time_paused: bool, game_tick: int. Precedence PAUSED>SLOWED>NORMAL; a superseded request is preserved, not dropped." + referenced_by: + - docs/architecture/adr-0002-time-control-service.md + added: 2026-08-18 + revised: "" + + - contract: persistence_contract + status: active + pattern: direct_call # SaveService calls each system's serialize_state()/deserialize_state() via injected references (composition root), never node/autoload lookup by name + producer: save-persistence # owns the file, envelope, checksum, migration chain + consumers: + - forja-de-legado # forge_counter (int64) + relic_records + - reliquias-bendiciones # invocation_charges_remaining + active_modifiers as remaining_ticks (NOT absolute expiry_tick); replaces the AC-RB46 mock on ADR acceptance + - temporizador-de-preparacion-ritual # elapsed_s / current_phase + - datos-de-era-civilizacion # active era_id (reference only, never copies EraDefinition) + - sistema-de-heroes # pending-death payload with sealed:bool for crash recovery (AC-H27c) + - transicion-de-era # era_id ACTIVE/ARCHIVED + adr: docs/architecture/adr-0003-save-persistence-serialization.md + signal_signature: "serialize_state() -> Dictionary (plain Variants only — no RefCounted/Resource/Object) / deserialize_state(data: Dictionary) -> void; SaveService.register_persistable(system_key: StringName, node: Node); request_save() -> bool; request_load() -> LoadResult{NO_SAVE_EXISTS,SAVE_EXISTS,CORRUPTED,VERSION_TOO_NEW}. Migrations run on raw Dictionary (pre-typed) before deserialize_state; schema_version>CURRENT -> VERSION_TOO_NEW (loud-fail, no migration)." + referenced_by: + - docs/architecture/adr-0003-save-persistence-serialization.md + added: 2026-08-21 + revised: "" + + - contract: era_lifecycle + status: active + pattern: shared_service # EraManager Autoload injected by reference; consumers read on the era_loaded signal or via get_active_era() + producer: era-lifecycle # owns the state machine, the sync load, and the centralized validation gate + consumers: + - sistema-de-tropas # reads unit_roster + max_band_size on era_loaded + - sistema-de-heroes # reads hero roster on era_loaded + - encuentro-con-kaiju # reads kaiju_definition on era_loaded + - temporizador-de-preparacion-ritual # reads approach_duration_s/imminent_window_s on era_loaded + - permadeath # reads get_hero_roster_count() on era_loaded (not _ready()) — TR-permadeath-017 + - reliquias-bendiciones # reads invocation_charges per era on era_loaded + - forja-de-legado # origin_era_id = era_id of the ACTIVE era (get_active_era()) + - save-persistence # boot: supplies era_id via request_load(); applies per-era state on era_loaded + adr: docs/architecture/adr-0004-era-lifecycle-init-ordering.md + signal_signature: "load_era(era_id: StringName) / activate_era() / archive_active_era() / get_active_era() -> EraDefinition; signals era_loaded(era: EraDefinition), era_activated(era), era_archived(era), era_load_failed(era_id: StringName, reasons: PackedStringArray). Sync ResourceLoader.load(); null return -> FAILED('era_resource_not_found'). Centralized validation gate runs before era_loaded; any violation -> FAILED, era_loaded NOT emitted, never ACTIVE. Boot: composition root must be ancestor of all era_loaded consumers and call load_era() from its own _ready() (avoids missed-signal race under sync load)." + referenced_by: + - docs/architecture/adr-0004-era-lifecycle-init-ordering.md + added: 2026-08-21 + revised: "" # Example: # @@ -131,7 +247,28 @@ performance_budgets: [] # Register an API decision when the choice is non-obvious and other ADRs might # make a different choice for the same purpose without knowing this was decided. -api_decisions: [] +api_decisions: + - purpose: save_serialization + status: active + api: FileAccess.store_var / FileAccess.get_var # binary Variant serialization of a nested Dictionary + not: JSON.stringify / JSON.parse # explicitly rejected for save payloads + adr: docs/architecture/adr-0003-save-persistence-serialization.md + reason: "store_var/get_var preserves GDScript int (64-bit) with full fidelity; JSON represents all numbers as 53-bit doubles, which silently truncates forge_counter (Forja OQ-FL4 pt.3) in long legacies. Uniform across all fields — no per-field int64 encoding rule to forget. get_var(allow_objects=false) always; systems hand SaveService plain-Variant dicts, never RefCounted/Resource instances." + referenced_by: + - docs/architecture/adr-0003-save-persistence-serialization.md + added: 2026-08-21 + revised: "" + + - purpose: save_corruption_checksum + status: active + api: hash() # @GlobalScope Variant hash over the payload Dictionary + not: manual CRC32 + adr: docs/architecture/adr-0003-save-persistence-serialization.md + reason: "GDD requires corruption/partial-write detection only, not tamper resistance or cross-engine-version stability. hash() is built-in (no manual CRC32 code) and adequate. Note: 32-bit hash (~1-in-4e9 collision) — acceptable for the stated requirement; algorithm is an interchangeable detail if more margin is ever wanted. Checksum stored as a sibling key {payload, checksum}, never inside the hashed dict." + referenced_by: + - docs/architecture/adr-0003-save-persistence-serialization.md + added: 2026-08-21 + revised: "" # Example: # @@ -157,7 +294,113 @@ api_decisions: [] # Register a pattern when an ADR explicitly bans it AND other ADRs might # unknowingly use it (i.e., it's a tempting but wrong approach for this project). -forbidden_patterns: [] +forbidden_patterns: + - pattern: runtime_mutation_of_shared_combatprofile + status: active + description: "No system may mutate a CombatProfile Resource at runtime. It is + per-type shared content, read-only. Per-instance stat changes go through + CombatState modifiers instead. Keep CombatProfile.resource_local_to_scene = false." + why: "CombatProfile is shared across every instance of a unit type; mutating it + changes all instances and corrupts the source-of-truth balance data. Silent and + hard to trace." + adr: docs/architecture/adr-0001-combat-stat-modifier-layer.md + added: 2026-08-18 + + - pattern: effective_combat_stat_computed_outside_combate + status: active + description: "Only Combate/Daño computes a unit's effective combat stat and owns + the modifier lifecycle (apply + expiry). Other systems (e.g. Reliquias) supply + modifiers via add_modifier/remove_by_source but must not compute effective damage + themselves or hold the authoritative modifier stack." + why: "Combate Regla 1 = single damage path. Computing effective stats elsewhere + forks the damage math, and having a higher layer own the stack inverts the + Reliquias->Combate dependency into a cycle." + adr: docs/architecture/adr-0001-combat-stat-modifier-layer.md + added: 2026-08-18 + + - pattern: engine_native_pause_or_timescale_for_game_time + status: active + description: "Do not use SceneTree.paused or Engine.time_scale to implement the + game-time regime (pause / offering slow-mo). Sources request a regime from + TimeControl (request_regime/release_regime); consumers poll TimeControl.time_scale / + game_time_paused / game_tick." + why: "SceneTree.paused halts ALL process callbacks, making it impossible to + selectively exempt the death-beat clock and UI without per-node process_mode + gymnastics; Engine.time_scale scales physics globally (incl. systems that must run + real-time) and Engine.get_physics_frames() is not pause-aware. The cooperative + flag+scale model was chosen deliberately to allow selective exemption." + adr: docs/architecture/adr-0002-time-control-service.md + added: 2026-08-18 + + - pattern: wallclock_delta_for_game_time_simulation + status: active + description: "Game-time simulation logic must not advance on _process(delta) / wall-clock. + Read TimeControl (time_scale, game_tick, game_time_paused) instead. Exempt systems + (death-beat clock, UI/HUD) run real-time by design and explicitly do not read game_time." + why: "A system on wall-clock delta desyncs from pause and slow-mo — it keeps moving while + the world is paused/slowed. Enforced by CI grep for _process(delta) in game-time sim scripts." + adr: docs/architecture/adr-0002-time-control-service.md + added: 2026-08-18 + + - pattern: json_for_int64_persistence + status: active + description: "Do not serialize save data (or any int64 field) via JSON.stringify/parse. + Use FileAccess.store_var/get_var. JSON represents numbers as 53-bit doubles and + silently truncates 64-bit ints (e.g. forge_counter)." + why: "forge_counter (Forja OQ-FL4) guarantees relic_id uniqueness across a long legacy; + a silent truncation past 2^53 corrupts that uniqueness with no error. store_var + preserves GDScript's 64-bit int uniformly, with no per-field encoding rule to forget." + adr: docs/architecture/adr-0003-save-persistence-serialization.md + added: 2026-08-21 + + - pattern: persisting_absolute_game_tick + status: active + description: "No system's serialize_state() may persist TimeControl.game_tick or an + absolute StatModifier.expiry_tick. Persist remaining_ticks (relative: expiry_tick − + game_tick, computed at save time); on load, rebase as expiry_tick = game_tick (fresh 0) + + remaining_ticks." + why: "game_tick is a per-session counter by design (ADR-0002) — it freezes on pause, + scales with time_scale, and resets to 0 each session. An absolute tick persisted from a + prior session is meaningless against a reset counter. Enforced by CI grep for game_tick/ + expiry_tick outside time_control.gd and the dedicated save-rebasing code." + adr: docs/architecture/adr-0003-save-persistence-serialization.md + added: 2026-08-21 + + - pattern: deserializing_objects_from_save + status: active + description: "serialize_state() must return only plain Dictionary/Array/primitive Variants — + never RefCounted/Resource/Object instances (e.g. a live StatModifier). store_var/get_var + keep full_objects/allow_objects at their default false. Systems convert state to plain + dicts before handing it to SaveService." + why: "Deserializing Objects from a file is an attack surface — a crafted save could + instantiate arbitrary objects, not just a style preference. Enforced by CI grep for + store_var(..., true) / get_var(..., true) in save code." + adr: docs/architecture/adr-0003-save-persistence-serialization.md + added: 2026-08-21 + + - pattern: reading_eradefinition_in_ready + status: active + description: "No system may read EraDefinition fields (unit_roster, max_band_size, + kaiju_definition, get_hero_roster_count, invocation_charges, approach_duration_s, etc.) + in its _ready()/Autoload init ASSUMING an era is already loaded at boot. Boot consumers + read in the era_loaded(era) handler. Legit exception: a node instantiated post-boot may + read via EraManager.get_active_era() in its _ready() once an era is ACTIVE." + why: "Autoload _ready() runs at engine boot, before any era is LOADED — a read there returns + null/garbage. Generalizes Permadeath's era-LOADED contract to all consumers. Enforced by + CI grep for EraDefinition field reads in _ready() (carving out get_active_era())." + adr: docs/architecture/adr-0004-era-lifecycle-init-ordering.md + added: 2026-08-21 + + - pattern: era_lifecycle_driven_outside_eramanager + status: active + description: "Only EraManager drives the era state machine and loads EraDefinition resources. + No system loads an EraDefinition directly, transitions era state, or emits era lifecycle + signals. Consumers read via era_loaded / get_active_era() and never instantiate/mutate eras." + why: "A single owner guarantees exactly-one-ACTIVE, the centralized validation gate (a broken + era never partially activates), and a canonical era_loaded emitter. Distributed loading is + the broken pre-ADR state (N loads, scattered loud-fail, no ownership of transitions)." + adr: docs/architecture/adr-0004-era-lifecycle-init-ordering.md + added: 2026-08-21 # Example: # diff --git a/tests/README.md b/tests/README.md new file mode 100644 index 0000000000..b777618b64 --- /dev/null +++ b/tests/README.md @@ -0,0 +1,73 @@ +# Test Infrastructure + +**Engine**: Godot 4.6 +**Test Framework**: GdUnit4 +**CI**: `.github/workflows/tests.yml` +**Setup date**: 2026-08-21 + +> **Nota de framework**: el proyecto usa **GdUnit4** (MikeSchulze), consistente con el +> comando de CI de `.claude/docs/coding-standards.md` (`godot --headless --script +> tests/gdunit4_runner.gd`). Si `.claude/docs/technical-preferences.md` aún dice +> "GUT (Godot Unit Test)", es una entrada stale — debe decir GdUnit4. + +## Directory Layout + +``` +tests/ + unit/ # Isolated unit tests (formulas, state machines, logic) + integration/ # Cross-system and save/load tests + smoke/ # Critical path test list for /smoke-check gate + evidence/ # Screenshot logs and manual test sign-off records +``` + +## Installing GdUnit4 + +``` +1. Open Godot → AssetLib → search "GdUnit4" → Download & Install +2. Enable the plugin: Project → Project Settings → Plugins → GdUnit4 ✓ +3. Restart the editor +4. Verify: res://addons/gdunit4/ exists +``` + +## Running Tests + +Headless (CI and `/smoke-check`): + +``` +godot --headless --script tests/gdunit4_runner.gd +``` + +In-editor: open the GdUnit4 dock (bottom panel) and run a test suite or the whole +`tests/` tree. + +## Test Naming + +- **Files**: `[system]_[feature]_test.gd` +- **Functions**: `test_[scenario]_[expected]` +- **Example**: `combat_damage_test.gd` → `test_base_attack_returns_expected_damage()` + +## Determinism & Isolation (project testing-standards) + +- No random seeds, no time-dependent assertions — same result every run. +- Each test sets up and tears down its own state; no cross-test ordering. +- No file I/O, DB, or external APIs in unit tests — use dependency injection. +- Float comparisons use a tolerance (`assert_almost_eq` / `±0.0001`), never `==` + (Temporizador AC-T11, Forja EPSILON_TIER, etc.). +- Injectable seeded `RandomNumberGenerator` for any RNG (Kaiju AC-K71, Reliquias + AC-RB18); never global `randf`/`randi`. + +## Story Type → Test Evidence + +| Story Type | Required Evidence | Location | Gate | +|---|---|---|---| +| Logic | Automated unit test — must pass | `tests/unit/[system]/` | BLOCKING | +| Integration | Integration test OR playtest doc | `tests/integration/[system]/` | BLOCKING | +| Visual/Feel | Screenshot + lead sign-off | `tests/evidence/` | ADVISORY | +| UI | Manual walkthrough OR interaction test | `tests/evidence/` | ADVISORY | +| Config/Data | Smoke check pass | `production/qa/smoke-*.md` | ADVISORY | + +## CI + +Tests run automatically on every push to `main` and on every pull request. +A failed test suite blocks merging (testing-standards: never skip/disable a +failing test to make CI pass — fix the underlying issue). diff --git a/tests/evidence/.gitkeep b/tests/evidence/.gitkeep new file mode 100644 index 0000000000..cc082d1171 --- /dev/null +++ b/tests/evidence/.gitkeep @@ -0,0 +1,2 @@ +# Screenshot logs and manual test sign-off records (Visual/Feel + UI evidence). +# One file per story, named [story-id]-evidence.md, plus screenshots. diff --git a/tests/gdunit4_runner.gd b/tests/gdunit4_runner.gd new file mode 100644 index 0000000000..8843be3b26 --- /dev/null +++ b/tests/gdunit4_runner.gd @@ -0,0 +1,13 @@ +# GdUnit4 test runner — invoked by CI and /smoke-check +# Usage: godot --headless --script tests/gdunit4_runner.gd +extends SceneTree + +func _init() -> void: + var runner := load("res://addons/gdunit4/GdUnitRunner.gd") + if runner == null: + push_error("GdUnit4 not found. Install via AssetLib or addons/.") + quit(1) + return + var instance = runner.new() + instance.run_tests() + quit(0) diff --git a/tests/integration/.gdignore_placeholder b/tests/integration/.gdignore_placeholder new file mode 100644 index 0000000000..b786b9e92d --- /dev/null +++ b/tests/integration/.gdignore_placeholder @@ -0,0 +1 @@ +# Integration tests go here — one subdirectory per system diff --git a/tests/smoke/critical-paths.md b/tests/smoke/critical-paths.md new file mode 100644 index 0000000000..6a75375f3e --- /dev/null +++ b/tests/smoke/critical-paths.md @@ -0,0 +1,33 @@ +# Smoke Test: Critical Paths + +**Purpose**: Run these checks in under 15 minutes before any QA hand-off. +**Run via**: `/smoke-check` (which reads this file) +**Update**: Add new entries when new core systems are implemented. + +## Core Stability (always run) + +1. Game launches to main menu without crash +2. New game / session can be started from the main menu +3. Main menu responds to all inputs without freezing + +## Core Mechanic (update per sprint — "What the Gods Left Behind" MVP loop) + + +4. [Input/Control] Se puede seleccionar tropas/héroes y darles órdenes de movimiento (clic + box-select; prioridad héroe>tropa) +5. [Temporizador] El reloj de preparación avanza PREPARATION→IMMINENT→CLIMAX y dispara `entered_climax` +6. [Combate] `apply_damage` reduce vida y una tropa/héroe muere en el cruce por 0 sin error +7. [Kaiju] El kaiju aparece en `entered_climax`, telegrafía, y `STRIKING` aplica daño +8. [Permadeath] La muerte de un héroe dispara el beat `DEATH_HOLD` (pausa cooperativa vía TimeControl) y se resuelve +9. [Legado] La muerte forja una `RelicRecord` de extremo a extremo (permadeath → forja) +10. [Reliquias] Con pool pre-sembrado, una invocación en CLIMAX ofrece 3-choose-1 y aplica el efecto elegido + +## Data Integrity + +11. Guardado completa sin error (autosave en `DYING`; escritura atómica) — una vez implementado ADR-0003 +12. Cargar restaura el estado correcto (era_id, reliquias, `elapsed_s`/fase recomputada) — una vez implementado ADR-0003 +13. Recuperación por crash: un `DYING` persistido se coacciona a `DEAD` y forja su reliquia al cargar (AC-H27c) + +## Performance + +14. Sin caídas visibles de framerate en hardware objetivo (target 60fps) con banda + esbirros en CLIMAX +15. Sin crecimiento de memoria sostenido en 5 min de loop central diff --git a/tests/unit/.gdignore_placeholder b/tests/unit/.gdignore_placeholder new file mode 100644 index 0000000000..a6913c1e4f --- /dev/null +++ b/tests/unit/.gdignore_placeholder @@ -0,0 +1 @@ +# Unit tests go here — one subdirectory per system (e.g., tests/unit/combate/) diff --git a/tests/unit/combate/combat_damage_test.gd b/tests/unit/combate/combat_damage_test.gd new file mode 100644 index 0000000000..da3a57edc0 --- /dev/null +++ b/tests/unit/combate/combat_damage_test.gd @@ -0,0 +1,33 @@ +# Example / bootstrap test — proves the GdUnit4 harness + CI run green from day one. +# SELF-CONTAINED: it does not depend on game code (none exists yet). When Combate/Daño +# is implemented (see docs/architecture/adr-0001-combat-stat-modifier-layer.md), replace +# the local `_apply_damage` helper with the real single damage path and keep the cases. +# +# Naming convention: file [system]_[feature]_test.gd; function test_[scenario]_[expected]. +class_name CombatDamageTest +extends GdUnitTestSuite + + +# Combate/Daño Fórmula F1: health_after = clamp(health_before - attack_damage, 0, max_health) +# Overkill is discarded (clamp to 0), health is never negative (combate-dano.md Regla 5 / F1). +static func _apply_damage(health_before: int, attack_damage: int, max_health: int) -> int: + return clampi(health_before - attack_damage, 0, max_health) + + +func test_base_attack_returns_expected_damage() -> void: + # Defender at 40 HP takes 12 -> 28. No mitigation, no multiplier (Regla 5). + assert_int(_apply_damage(40, 12, 40)).is_equal(28) + + +func test_overkill_clamps_to_zero_never_negative() -> void: + # 999 damage on a 5 HP unit -> 0, never -994 (F1 / AC-C17/C18). + assert_int(_apply_damage(5, 999, 200)).is_equal(0) + + +func test_exact_lethal_hit_reaches_zero() -> void: + # Death triggers at the 0-crossing (<= threshold, not <): 40 - 40 -> 0 (is_dead true). + assert_int(_apply_damage(40, 40, 40)).is_equal(0) + + +func test_zero_damage_leaves_health_unchanged() -> void: + assert_int(_apply_damage(120, 0, 200)).is_equal(120)