Human form has a distinct **parry action** in addition to its standard attack. This is the mechanical realization of the "geometry catching attacks" fantasy.
**Parry Input**: Dedicated parry input (separate from attack) — player holds parry to enter a brief parry stance.
**Parry Window**: The parry sector (Angle=100°, Radius=3.0) is active for 0.15s from input. During this window:
- If **any enemy attack shape** overlaps the parry sector, the parry **succeeds**: the enemy is staggered (0.3s stun), the attack is nullified, and the next Human attack within 0.5s is a **guaranteed crit** (critMultiplier applied, no RNG roll).
- If the parry window expires with no enemy attack overlap, the player enters a 0.2s recovery (cannot act). This is the risk of parrying at the wrong time.
**Parry vs. Attack**: Human form can either attack (standard Sector sweep, base damage 18) or parry (defensive, no damage on parry itself, enables riposte crit). The player chooses which based on reading the battlefield.
### Positional Crit System
Crit is no longer RNG. Crit is earned through positioning, replacing the previous `random(0,1) < critChance` formula.
| Form | Crit Condition | Crit Behavior |
|------|---------------|---------------|
| **Human** | Parry success → next attack within 0.5s is guaranteed crit. Also: attacking enemy from behind (flanking, angle > 120° between enemy facing and attack origin) grants +30% crit chance from critChanceStat. | critMultiplier applied |
| **Wolf** | Dash hitting 3+ enemies in single activation → all hits in that dash are crit. Also: hitting enemy from outside its forward 60° cone (flanking) grants +30% crit chance from critChanceStat. | critMultiplier applied to all dash hits |
| **Mist** | Enemy within inner 40% of circle radius (≤1.52 units from center) → guaranteed crit. Also: hitting enemy from behind (flanking, angle > 120°) grants +30% crit chance from critChanceStat. | critMultiplier applied to inner-radius hits |
**critChanceStat**: A per-form stat (base 0.05, grows via Skill Tree to 0.0–0.30) that adds flat crit chance on top of positional conditions. At 0.05 base, a flanking attack has 35% crit chance (30% positional + 5% stat). This preserves a small RNG element that grows with investment — but positional crits (parry, dash-chain, inner-radius) are always 100% regardless of stat.
**Angle Convention**: All angles in AttackData and EnemyState are in **degrees** (matching Unity convention). L0's math library (ADR-003) converts to radians internally before calling `System.Math` trig functions. This convention must be documented in ADR-003.
**Vector3 Boundary**: L0 uses custom `Vector3` (ADR-003). L1 converts to `UnityEngine.Vector3` via a static utility class `Vector3Conversion.ToUnity(this Combat.Vector3)` in the L1 Adapters assembly. L0 never references `UnityEngine.Vector3`.
**HitPoint Coordinate Space**: `DamageResult.HitPoint` is in **L0 world coordinates** (same coordinate system as `AttackData.Origin` and `EnemyState.Position`). L1 converts to Unity world space for VFX positioning. L0 computes it from the intersection point of the attack shape and enemy hit shape.
**Random Number Injection**: Combat Logic accepts an `IRandomProvider` interface with `double NextDouble()` for critChanceStat rolls. Default implementation wraps `System.Random`. Tests inject a seeded or mock provider for deterministic crit testing.
**Shape Polymorphism**: Shape types use a **union struct** pattern (`HitShape` with `ShapeType` enum + all shape fields) to avoid boxing and virtual dispatch in the hot path.
```
ShapeType { Circle, Rect, Sector }
HitShape {
ShapeType Type;
Circle Circle;
Rect Rect;
Sector Sector;
bool Contains(Vector3 point) { /* switch by Type, no boxing */ }
bool Intersects(HitShape other) { /* dispatch by Type pair */ }
}
```
Epsilon constant: `const float HIT_EPSILON = 0.001f` — overlap must exceed this value to count as a hit. Defined in ADR-003.
### AttackData Construction
An L0 factory method constructs `AttackData` from `AttackStyle` + input intent:
L1's InputAdapter collects the raw data (player transform position, facing, input state) and calls this factory. The factory computes the hit shape geometry from AttackStyle parameters — this is game logic, appropriate for L0.
### Hit Resolution Pipeline (called once per frame)
2. CULL: Remove dead enemies, skip enemies outside attack range (coarse filter)
2. CULL: Remove dead enemies, skip enemies outside attack range (coarse filter via spatial grid)
3. INTERSECT: For each attack × potential target, call ADR-003 Shape.Intersects()
4. DAMAGE: For each hit pair, execute damage formula (Section D)
5. SORT: Descending by damage — high damage resolves first
6. DEDUPLICATE: Same attack can hit multiple enemies; same enemy can be hit by multiple attacks — but each attack hits each enemy at most once per frame
**Pipeline is batch-per-frame**: All attacks are resolved against pre-frame enemy state. Dead filtering (step 2) runs once at the start. If an enemy is alive at frame start, all attacks that intersect it will produce hits this frame — damage is summed and applied downstream once after resolution. There is NO mid-frame kill check.
**Sorting**: Results are ordered by AttackId (stable, deterministic). There is no sort-by-damage step.
**Spatial Grid**: A uniform spatial grid (L0 pure C#, cell size = max attack range) provides broad-phase culling. Enemies are bucketed at O(E) per frame. Attack queries check only nearby cells, reducing intersection pairs from O(A×E) to O(A×local-density).
**Memory**: Use `NonAlloc` pattern — caller provides a pre-allocated `List<DamageResult>` buffer that `ResolveAttacks` fills in-place. No per-frame allocation from Combat Logic.
### Target Filtering Rules
- Enemy state = Dead → skip
- Attack shape has no intersection with enemy hit shape → skip
- Same attack already hit this enemy this frame → skip (dedup)
- Same (AttackId, TargetId) pair already processed this frame → skip (dedup)
| `OnHit` | Every successful hit | `DamageResult` |
| `OnKill` | Hit reduces enemy health to 0 | `DamageResult` |
| `OnCrit` | Hit triggers critical | `DamageResult` |
| `OnAttackResolved` | Attack resolution complete (fires even if 0 hits) | `AttackData, int hitCount` |
| `OnKill` | Hit reduces enemy health to ≤0 | `DamageResult` |
| `OnCrit` | Hit triggers critical (positional or stat-based) | `DamageResult` |
| `OnParrySuccess` | Human parry intersects enemy attack during parry window | `AttackData, DamageResult` |
| `OnAttackResolved` | Per-attack resolution complete (fires even if 0 hits) | `AttackData, int hitCount` |
**Event batching note**: For performance at high hit density, L1 subscribers (VFX Spawner, Audio Player) should batch-process events per frame and apply per-frame caps (e.g., max 200 particle bursts, max 20 SFX). See Performance Considerations below.
### Interactions with Other Systems
| System | Direction | Interface |
|--------|-----------|-----------|
| Form Switch SM | Upstream | Reads `AttackStyle` for shape + base damage per form |
| Enemy AI Logic | Downstream | Enemy receives damage, updates health, may trigger behavior change |
| VFX Spawner (L1) | Downstream | Subscribes to `OnHit`/`OnKill`/`OnCrit` for visual feedback |
| Blood Energy Economy | Downstream | `OnHit` triggers `BloodEnergyEconomy.Add(attackGain)`, `OnKill` triggers `BloodEnergyEconomy.Add(killGain)`. Connected via L1 `BloodEnergyBridge` adapter that subscribes to Combat Logic events and calls Blood Energy Economy methods. |
| Enemy AI Logic | Downstream | Enemy receives `DamageResult` for health subtraction, behavior response, and resistance application |
| VFX Spawner (L1) | Downstream | Subscribes to `OnHit`/`OnKill`/`OnCrit`/`OnParrySuccess` for visual feedback |
| Death/Respawn | Downstream | `OnKill` with IsKillingBlow triggers death check |
| ADR-003 Geometry | Foundation | Uses `Circle`, `Rect`, `Sector` for all intersection tests |
**Design Intent**: Combat Logic applies only base damage. Enemy resistance modifiers and form affinity bonuses are defined in Enemy AI GDD. Skill damage bonuses are defined in Skill Tree GDD. This keeps Combat Logic single-responsibility.
**Design Intent**: Combat Logic applies only base damage× critFactor. Enemy resistance modifiers are defined in Enemy AI GDD and applied downstream. Skill damage bonuses are defined in Skill Tree GDD. This keeps Combat Logic single-responsibility.
**Output**: 0–15 units | Critical hits add 1.5× knockback
**Output**: 0–15 units | Critical hits add 1.5× knockback. `max(enemyWeight, 1.0)` prevents division by zero.
## Edge Cases
| # | Condition | Resolution | Rationale |
|---|-----------|------------|-----------|
| 1 | Attack shape has zero area (radius=0, width=0) | Skip, return empty list | Degenerate shape — no valid hit possible |
| 1 | Attack shape has zero area (radius=0, width=0, angle=0) | Skip entire attack, return empty results for that attack | Degenerate shape — no valid hit possible |
| 2 | Enemy has no hit shape defined | Skip that enemy | Incomplete data — should not happen at runtime |
| 3 | Same attack hits same enemy twice (edge overlap) | Dedup: count only first hit | One attack = one hit per target per frame |
| 4 | `critChance` exceeds 1.0 via buffs | Clamp to 1.0 | Guaranteed crit is the ceiling |
| 5 | `baseDamage` = 0 (status effect attack) | Still perform hit test, emit OnHit with 0 damage | Knockback may still apply |
| 6 | Multiple attacks in same frame against same enemy | Resolve all, sort by damage desc | Independent attacks stack |
| 7 | Enemy killed mid-frame by attack #1 | Attacks #2-N skip this enemy (dead filter) | Dead enemies don't accumulate damage |
| 8 | `knockbackForce` = 0 | Return 0, skip calculation | No force applied |
| 9 | deltaTime = 0 (pause) | Return empty list, no events | Paused game = no combat resolution |
| 10 | Empty enemies list | Return empty list, no events | No targets to hit |
| 11 | Shape intersection at exact tangent point | Count as miss (requires overlap > epsilon) | Prevents degenerate edge-touch = hit |
| 3 | Same (AttackId, TargetId) processed twice in one frame | Dedup: count only first occurrence | One attack = one hit per target per frame |
| 4 | `critChanceStat` exceeds 1.0 via buffs | Clamp to 1.0 (combined with positional bonus, cap at 1.0) | Guaranteed crit is the ceiling |
| 5 | `baseDamage` = 0 (status effect attack) | Still perform hit test, emit OnHit with FinalDamage=0 | Knockback may still apply; positional crit conditions still evaluated |
| 6 | Multiple attacks in same frame against same enemy | Resolve all — damage summed downstream | Independent attacks stack; all resolved against pre-frame state |
| 7 | `knockbackForce` = 0 | Return knockbackDistance=0, skip calculation | No force applied |
| 8 | deltaTime = 0 (pause) | Return empty list, no events | Paused game = no combat resolution |
| 9 | Empty enemies list | Return empty list, no events | No targets to hit. OnAttackResolved still fires per attack with hitCount=0. |
| 10 | Shape intersection at exact tangent point (overlap ≤ HIT_EPSILON) | Count as miss | HIT_EPSILON = 0.001f. Prevents degenerate edge-touch = hit |
| 11 | Parry activated but no enemy attack in window | Parry fails → 0.2s recovery, OnAttackResolved fires with hitCount=0 | Risk of parrying at wrong time |
| 13 | `Direction` passed as radians (incorrect, should be degrees) | L0 treats Direction as degrees; caller's bug | Convention enforcement: ADR-003 documents degrees as the interchange format |
## Dependencies
| System | Relationship | Interface |
|--------|-------------|-----------|
| Form Switch SM | Upstream | Reads `AttackStyle.BaseDamage`, `AttackStyle.AttackShape` per form |
| Blood Energy Economy | Downstream | `OnHit` → `Add(attackGain)`, `OnKill` → `Add(killGain)`. Bridged via L1 adapter. |
| Enemy AI Logic | Downstream | Enemy receives `DamageResult` for health subtraction and behavior response |
| VFX Spawner (L1) | Downstream | Subscribes to `OnHit`, `OnKill`, `OnCrit` for visual feedback |
| VFX Spawner (L1) | Downstream | Subscribes to `OnHit`, `OnKill`, `OnCrit`, `OnParrySuccess` for visual feedback |
| Death/Respawn | Downstream | `OnKill(IsKillingBlow=true)` triggers death check |
| ADR-003 Geometry | Foundation | Uses `Circle`, `Rect`, `Sector` for all intersection tests |
| ADR-003 Geometry | Foundation | Uses `Circle`, `Rect`, `Sector` for all intersection tests; defines HIT_EPSILON and degrees convention |
No upstream GDD dependencies — this is a Foundation layer system. Form Switch SM GDD is undesigned; the expected `AttackStyle` contract is defined here as provisional until that GDD is written.
@@ -153,14 +254,38 @@ No upstream GDD dependencies — this is a Foundation layer system. Form Switch
| Parameter | Default | Safe Range | Too Low | Too High |
**Key tuning pair**: `baseSwitchCost / baseAttackGain ≈ 8` (from Blood Energy Economy GDD) — roughly 8 attack hits to earn one switch. Base damage values here affect that rhythm: higher base damage means faster kills, faster energy gain, more frequent switching.
## Performance Considerations
**Spatial partitioning**: Uniform grid with cell size = max attack range (5.0 units). Enemies bucketed at O(E) per frame. Attack queries check only nearby cells, reducing intersection pairs ~10× at 200 enemies.
**Memory**: `NonAlloc` pattern — caller provides pre-allocated `List<DamageResult>`. No per-frame allocation from Combat Logic. Dedup uses pre-allocated `HashSet<(int AttackId, int TargetId)>` cleared per frame.
-`OnKill`: Enemy shatter into geometric fragments — fragment color = enemy type
-Debug overlay: Semi-transparent fill of Sector/Rect/Circle during attack frames
-`OnParrySuccess`: Sharp ring-shaped particle burst at parry contact point — white/silver flash
- Debug overlay: Semi-transparent fill of Sector/Rect/Circle during attack/parry frames (player-facing shape preview)
**Audio (via L1 Audio Player):**
-`OnHit`: Impact SFX — varies by form (Human=sharp slash, Wolf=heavy thud, Mist=ethereal whoosh)
-`OnCrit`: Distinct "clink" + bass emphasis
-`OnKill`: Shatter sound — pitch varies by enemy size
-`OnParrySuccess`: Metallic ring + short bass drop
**Art Bible Alignment**: Principle 1 (Color is Identity — hit VFX color = form color), Principle 3 (Particles are Feedback — geometric fragments = information).
@@ -183,30 +310,46 @@ Combat is a visual system — Visual/Audio is REQUIRED.
| Element | Position | Content |
|---------|----------|---------|
| Damage Numbers | Floating at hit point | Numeric value, color = form color, crit = larger + gold |
| Damage Numbers | Floating at hit point | Numeric value, color = form color, crit = larger + gold, positional crit = gold + "positional" icon |
| Hit Indicator | Screen edge | Directional flash when player takes damage |
| 9 | Enemy A { Health=0, IsDead=true }, Enemy B { Health=20 }. Attack covers both. | `ResolveAttacks([attack], [enemyA, enemyB], dt=0.016)` | Returns 1 result (Enemy B only). Enemy A skipped (dead). OnAttackResolved fires with hitCount=1. |
| 10 | critChanceStat=1.2 (beyond cap). | Crit resolution | Clamped to 1.0. With no flanking active, combined cap is 1.0. Mock RNG returns 0.99 → crit (0.99 < 1.0). |
| 11 | 3 attacks [A(AttackId=1), B(AttackId=2), C(AttackId=3)] all hitting same enemy (Health=100). | `ResolveAttacks([A, B, C], [enemy], dt=0.016)` | Returns 3 results (TargetId=1, AttackId distinct). Total FinalDamage = sum of 3 individual damages. All resolved against pre-frame health of 100. OnHit fires 3 times. |
| 12 | Attack misses all enemies (none in range). | `ResolveAttacks([attack], [enemies], dt=0.016)` | Returns empty list. OnAttackResolved fires with hitCount=0. No OnHit/OnKill/OnCrit. |
| 13 | Valid attack, enemy in range, but deltaTime=0.0. | `ResolveAttacks([attack], [enemy], dt=0.0)` | Returns empty list. No events fire. |
| 14 | Same (AttackId, TargetId) pair appears twice in pipeline (bug or edge overlap). | Dedup step | Only first occurrence produces a result. Second is skipped. OnHit fires once. |
| 15 | OnCrit event: any crit (positional guarantee or stat roll succeeds). | Hit resolves with IsCritical=true | OnCrit fires with DamageResult. Payload matches the crit hit. |
| 16 | Parry: AttackData { IsParry=true, HitShape=Sector(100°, 3.0) }. Enemy attack HitShape overlaps sector during 0.15s window. | `ResolveAttacks([parry], [enemyAttack], dt=0.016)` | OnParrySuccess fires. Next Human attack within 0.5s has positional crit guarantee. |
| 17 | Parry: same parry attack, but no enemy attack shape overlaps. | Same call | OnParrySuccess does NOT fire. OnAttackResolved fires with hitCount=0. |
| 18 | enemyWeight=0 (invalid). KnockbackForce=10. | `ResolveAttacks` | KnockbackDistance = 10.0 (10/max(0,1.0) = 10/1.0, non-crit). Formula guards against division by zero. |
| 19 | Attack shape with radius=0 (degenerate). | `ResolveAttacks([zeroAreaAttack], [enemies], dt=0.016)` | Returns empty list for that attack. OnAttackResolved fires with hitCount=0. |
| 20 | Empty enemies list but attack data present. | `ResolveAttacks([attack], [], dt=0.016)` | Returns empty list. OnAttackResolved fires per attack with hitCount=0. No OnHit/OnKill/OnCrit. |
## Open Questions
-Should enemies have different hit shape sizes (e.g., Brute larger than Swarm) or uniform? — Defer to Enemy AI GDD
-Should there be a "perfect timing" bonus for Human parry (hitting within narrow enemy attack window)? — Candidate for Skill Tree / Form Switch SM GDD
-Attack shape size scaling: should Skill Tree upgrades increase shape dimensions, or only damage? — Defer to Skill Tree GDD
-Parry window (0.15s) and recovery (0.20s) need prototype validation — actual game feel will determine the right durations
-Inner crit radius for Mist (40% of circle radius) — does this create the right "dense at center" feel or should it be a flat value?
-Should there be a "perfect parry" tier (tighter window, bigger reward) or is the single parry window sufficient?
- Should enemies have varying hit shape sizes (e.g., Brute larger than Swarm) or uniform radius? — Defer to Enemy AI GDD
- Attack shape size scaling: should Skill Tree upgrades increase shape dimensions, or only damage/critChanceStat? — Defer to Skill Tree GDD
- Spatial grid cell size: max attack range (5.0) may be too coarse for predominantly close-range combat. Profile during prototype.
- Blood Energy Economy is injected via constructor (`IBloodEnergyEconomy` interface) — follows project DI standard. `RequestSwitch` takes only `FormType target`.
| Recovery | ✓ | ✓ | ✗ | ✗ | New form color stabilizing |
**4. Cooldown**: After Recovery, extra 0.3s before next switch allowed. Windup(0.25) + Switching(0.10) + Recovery(0.15) + Cooldown(0.30) = 0.80s minimum between switches.
**Cooldown state note**: During cooldown, `Phase` reports `Idle` (the player can move and attack freely). Systems must query `CanSwitchTo(target)` — not `Phase` — to determine switch availability. `CanSwitchTo()` checks: Phase=Idle AND cooldown expired AND energy sufficient AND target≠current. `Phase` alone does not imply switch-readiness.
> **Canonical source**: BaseDamage and shape geometry values are owned by Combat Logic GDD (reviewed). Form Switch SM defines the AttackStyle struct and queries Combat Logic's per-form values at runtime. If values diverge, Combat Logic is authoritative.
### Interactions with Other Systems
| System | Direction | Interface |
|--------|-----------|-----------|
| Blood Energy Economy | Upstream | Calls `CanSpend(cost)`, `Spend(cost)` on successful switch |
| Player Input | Upstream | Responds to `OnSwitchPress(FormType)` events from Input Adapter |
| Combat Logic | Downstream | Provides `GetAttackStyle(CurrentForm)` for hit resolution |
| Skill Tree | Upstream | Can modify `WindupDuration`, `CooldownDuration`, unlock new forms |
**Event ordering contract**: When a switch completes (Recovery→Idle), events fire in this order: (1) `OnFormChanged(from, to)`, (2) `OnPhaseChanged(Idle)`. When `Interrupt()` is called during Windup: (1) `OnSwitchInterrupted(targetForm)`, (2) `OnPhaseChanged(Idle)`. During normal phase transitions (Windup→Switching→Recovery): only `OnPhaseChanged` fires. Subscribers (VFX, Audio, HUD) can rely on this ordering.
**Dependency injection**: `IBloodEnergyEconomy` is injected via constructor. `RequestSwitch(FormType target)` takes only the target form — it internally calls `_energy.CanSpend(cost)` and `_energy.Spend(cost)`. This follows the project DI-over-singletons standard and allows mock injection for L0 testing.
The timing parameters are tunable (see Tuning Knobs). The WindupDuration is the critical feel variable — it creates the "danger window" that defines the Switch is Commitment pillar.
## Edge Cases
| # | Condition | Resolution |
|---|-----------|------------|
| 1 | RequestSwitch while Phase≠Idle | Return OnCooldown — only Idle accepts input |
| 2 | RequestSwitch to current form | Return InvalidTarget — meaningless switch |
| 3 | Hit during Windup | Interrupt() → Phase=Idle, energy already spent, NOT refunded |
| 4 | Hit during Switching | Ignored — i-frames are absolute |
| 5 | Die during any phase | Force reset to Idle, CurrentForm=Human, energy already at 0 |
| 6 | skillDiscount makes switchCost→0 | Minimum cost 1.0 enforced (from Blood Energy GDD) |
| 7 | Switch key held down (repeated input) | Ignored — only rising edge triggers RequestSwitch |
| 9 | WindupDuration reduced to 0 by Skill Tree | Clamp to minimum 0.10s floor — Skill Tree cannot reduce WindupDuration below 0.10s |
| 10 | Cooldown reduced to 0 by Skill Tree | Clamp to minimum 0.15s floor — Skill Tree cannot reduce CooldownDuration below 0.15s |
| 11 | Damage during Windup exactly as timer expires | Switching wins — damage ignored (favor the player at boundaries). Implementation: within a single Update() call, the phase timer is decremented FIRST, then damage is processed. |
| 12 | Multiple switch requests in same frame | Only first processed |
| 13 | deltaTime exceeds WindupDuration (frame spike) | Clamp effective deltaTime to min(deltaTime, WindupDuration) — process at most one phase transition per Update() call |
| 14 | Skill Tree modifies timing param mid-switch | Snapshot at RequestSwitch — changes apply on next switch, not mid-transition |
| 15 | Switch input buffered during last 0.10s of Recovery | Queued input auto-executes when cooldown expires. Buffer holds exactly one input (most recent wins). |
## Dependencies
| System | Relationship | Hard/Soft |
|--------|-------------|-----------|
| Blood Energy Economy (Approved) | Upstream — must check/spend energy | Hard |
| Player Input (undesigned) | Upstream — input events | Hard |
| Combat Logic (Designed) | Downstream — provides AttackStyle per form | Hard |
Provisional assumption: Player Input provides `OnSwitchPress(FormType)` events. The Form-to-key mapping (Human=1, Wolf=2, Mist=3 or similar) is defined in the Input Adapter GDD.
**Key interaction**: Reducing WindupDuration + CooldownDuration too aggressively removes the tactical weight of switching. **Hard floors enforced**: `WindupDuration ≥ 0.10s`, `CooldownDuration ≥ 0.15s`, keeping total interval ≥ 0.40s (0.10+0.05+0.10+0.15). Skill Tree upgrades reduce toward these floors but never below them.
## Visual/Audio Requirements
Combat system — Visual/Audio is REQUIRED.
**VFX (via L1 VFX Spawner)**:
-`OnFormChanged(from, to)`: Geometric burst at player position — color = new form color
-`OnPhaseChanged(Windup)`: Form flicker + color lerp from current to target
**Art Bible Alignment**: Principle 1 (Color is Identity — switch VFX color directly communicates new form), Principle 3 (Particles are Feedback — each phase has a unique particle signature).
## UI Requirements
| Element | Content |
|---------|---------|
| Form Indicator | 3 form icons — current highlighted, out-of-reach greyed, available glowing |
| Switch Cooldown Ring | Circular timer showing remaining cooldown around form indicator |
| Switch Failure Feedback | InsufficientEnergy = energy bar flash red; OnCooldown = cooldown ring pulse; InvalidTarget = current form icon pulse (already active) |
| Buffered Switch Indicator | When switch input is queued during Recovery buffer: form icon shows subtle glow/border pulse to confirm input was registered |
| 10 | Phase=Windup, energy deducted by Spend(cost) on switch start | Interrupt() | Phase→Idle, energy NOT refunded (verify mock IBloodEnergyEconomy.Refund() was NOT called), OnSwitchInterrupted fires with target form |
| 11 | Phase=Switching | Hit received / Interrupt() | Ignored — Phase stays Switching, no events fire |
| 12 | Phase=Recovery | Hit received | Ignored — Phase stays Recovery |
| 13 | Phase=Windup, PhaseTimer≤PHASE_EPSILON (0.001), hit received same frame | Update(dt) then damage check | Phase→Switching before damage processed — hit ignored (player-favored boundary) |
| 30 | Phase=Windup, Die() called | Death during switch | Force reset: Phase=Idle, CurrentForm=Human. Energy NOT refunded. |
| 31 | deltaTime=0.0 | Update(0) | Phase unchanged, PhaseTimer unchanged, no events fire |
| 32 | deltaTime=0.3 (exceeds WindupDuration=0.25) | Update(0.3) | Effective dt clamped to WindupDuration — at most one phase transition. Phase→Switching, PhaseTimer=SwitchDuration. |
| 33 | Two RequestSwitch(Wolf) calls in same frame (before Update) | Sequential calls | First returns Success (Phase→Windup). Second returns OnCooldown (Phase≠Idle). |
| 35 | Two different form requests buffered during same Recovery window | RequestSwitch(Wolf) then RequestSwitch(Mist) | Only most recent (Mist) is queued — buffer holds one input. |
| 36 | Phase=Idle, cooldown active, RecoveryTimer already 0 (outside buffer window) | RequestSwitch(Wolf) | Returns SwitchResult.OnCooldown — NOT buffered. Buffer only active during last 0.10s of Recovery. |
## Open Questions
- Should the player be able to buffer a switch request during Recovery (auto-execute after cooldown)? — **Yes**: implement a 0.10s buffer window at the end of Recovery. Any switch input during the last 0.10s of Recovery is queued and auto-executes when the cooldown expires. Short enough to require timing, long enough to prevent dropped-input frustration. The buffer holds exactly one input (most recent wins).
- Should Windup slow use a configurable curve (linear vs ease-in)? — MVP uses linear; revisit for polish.
- Should the architecture support a 4th form in the future? — Yes, `FormType` enum is extensible. Skill Tree GDD should reserve an expansion node.
Summary: The hit-detection infrastructure (shapes, pipeline, edge cases, events) was solid, but three fundamental gaps blocked implementation: (1) the Player Fantasy described a parry mechanic that didn't exist in the design, (2) pure RNG crit contradicted the "Skill Over Stats" pillar, and (3) cross-system contracts (EnemyState, Vector3 boundary, AttackData construction, Blood Energy bridge) were undefined. All 13 blockers were resolved in the same session via targeted additions: full parry subsystem, positional crit system, defined structs, L0/L1 boundary contracts, spatial grid, and NonAlloc memory pattern. Form dimensions rebalanced (Mist 3.8, Wolf width 1.0, Human angle 100°). GDD now ready for implementation pending a lightweight re-review pass.
**Summary**: The core 4-phase FSM design is sound, but the GDD carried six blocking issues that made it unsafe to implement as written. The critical finding was an AttackStyle data conflict with the Combat Logic GDD — 5 of 6 parameters (damage values, shape geometry) disagreed between the two documents. Additional blocking issues: self-contradictory minimum interval guard (Skill Tree could reduce to 0.15s vs stated ≥0.40s floor), no input buffering in a 0.45s dead zone, ambiguous cooldown state representation, three conflicting API signatures across documents, and GetAttackStyle() missing from the architecture contract. The creative-director noted the concept is solid and needs a focused revision pass, not a rewrite.
---
## Review — 2026-04-27 — Verdict: APPROVED (revised, same session)
**Scope signal**: M
**Specialists**: (revision pass — no re-review agents spawned)
**Summary**: All six blocking items were resolved in a single revision pass. AttackStyle values were reconciled with Combat Logic GDD (Combat Logic is canonical source for numbers). Hard floors added (WindupDuration ≥ 0.10s, CooldownDuration ≥ 0.15s) enforcing the ≥0.40s interval guard. 0.10s input buffer window added at end of Recovery. Cooldown state clarified with CanSwitchTo() as authoritative query. API standardized to constructor-injected IBloodEnergyEconomy. GetAttackStyle() contract aligned. ACs expanded from 12 to 36 covering all phase transitions, events, phase capabilities, edge cases, and input buffering. GDD is ready for implementation.
| 3 | Form Switch State Machine | L0 | Combat Logic | MVP | Approved | [GDD](form-switch-state-machine.md) | Blood Energy Economy |
| 4 | Enemy AI Logic | L0 | Combat Logic | MVP | Not Started | — | Combat Logic |
| 5 | Boss AI Logic | L0 | Combat Logic | MVP | Not Started | — | Enemy AI Logic, Combat Logic |
| 6 | Wave Manager Logic | L0 | Combat Logic | MVP | Not Started | — | Enemy AI Logic |
@@ -144,18 +144,20 @@
| Metric | Count |
|--------|-------|
| Total systems identified | 17 |
| Design docs started | 2 |
| Design docs reviewed | 0 |
| Design docs approved | 1 |
| MVP systems designed | 1/14 |
| Design docs started | 3 |
| Design docs reviewed | 2 |
| Design docs approved | 2 |
| MVP systems designed | 3/14 |
| Vertical Slice systems designed | 0/3 |
---
## Next Steps
- [] Design MVP-tier systems in dependency order — start with Blood Energy Economy
- [] Run `/design-system Blood Energy Economy`to author the first GDD
- [x] Design MVP-tier systems in dependency order — Blood Energy Economy and Combat Logic designed
- [x] Run `/design-system Blood Energy Economy`— Complete (Approved)
- [x] Run `/design-system Combat Logic` — Complete (Reviewed, revised)
- [x] Run `/design-system Form Switch State Machine` — Complete (Approved, revised after review)
- [ ] Run `/prototype form-switch-state-machine` on the highest-risk system early
- [ ] Run `/design-review` on each completed GDD
- [ ] Run `/design-review` on remaining GDDs
- [ ] Run `/gate-check pre-production` when all MVP GDDs are authored and reviewed
Reference in New Issue
Block a user
Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user.