# Milestone 2 Investigation Report: Visceral Combat Feel Engine

**Subject**: Deep-dive analysis and implementation strategy for Micro Hit-Stop, Directional Camera Shake Matrix, and Static Ring Buffer Damage Number Pool (`combat_feel_engine.js`).  
**Investigator**: `explorer_m2_2_gen3`  
**Target Milestone**: Milestone 2 (Character Animation & Visceral Combat Feel Engine)  
**Working Directory**: `c:\Projects\FreeExile\.agents\teamwork\explorer_m2_2_gen3`

---

## Executive Summary
`combat_feel_engine.js` exists at `client/webapp/js/engine/combat_feel_engine.js` (198 lines, complying with the $\le 350$ lines soft cap). While the foundational architecture (`DamageNumberSlot` + `CombatFeelEngine`) is structurally sound, the module is currently partially orphaned from the live render and combat execution loops:
1. `isHitStopped(dt)` contains a critical NaN edge-case bug when called without arguments (`dt === undefined`), which breaks the timer permanently.
2. The pop-in bounce scale for standard damage numbers is currently implemented as $1.4 \rightarrow 1.0$ instead of the canonical $1.6 \rightarrow 1.0$ specification.
3. Screen shake intensity and duration lack strict clamping to the canonical ranges ($2.5 - 8.5$ intensity, $0.12 - 0.45\text{s}$ duration) and do not support directional bias along the strike vector.
4. The live gameplay loop in `canvas_renderer.js` and `vfx_renderer.js` does not invoke `combatFeelEngine.update(dt)` or `combatFeelEngine.renderDamageNumbers(ctx)`, instead continuing to allocate heap objects via legacy `damageTexts.push()` and `.splice()`.

---

## 1. Observation

### 1.1 Existing Implementation of `combat_feel_engine.js`
- **File path**: `client/webapp/js/engine/combat_feel_engine.js` (Total 198 lines).
- **Module script inclusion**: `client/webapp/index.html` line 189:
  ```html
  <script type="module" src="js/engine/combat_feel_engine.js"></script>
  ```
- **Current `DamageNumberSlot` implementation** (lines 11-97):
  ```javascript
  init(text, x, y, color = '#ffffff', isCrit = false) {
    this.active = true;
    this.text = String(text);
    this.x = x + (Math.random() - 0.5) * 16;
    this.y = y - 10;
    this.startY = this.y;
    this.color = color;
    this.isCrit = isCrit;
    this.age = 0;
    this.lifetime = isCrit ? 0.95 : 0.70;
    this.scale = isCrit ? 1.8 : 1.3;
    this.alpha = 1.0;
  }
  ```
  Lines 52-58:
  ```javascript
  // Pop-in bounce scale (1.6x down to 1.0x in first 20% of lifetime)
  if (progress < 0.2) {
    const p = progress / 0.2;
    this.scale = (this.isCrit ? 1.8 : 1.4) - p * (this.isCrit ? 0.6 : 0.4);
  } else {
    this.scale = this.isCrit ? 1.2 : 1.0;
  }
  ```
  *Observed deviation*: Standard hits start at scale $1.4$ and bounce down by $0.4$ to $1.0$. The project specification states $1.6 \rightarrow 1.0$.

### 1.2 Micro Hit-Stop Timer & NaN Defect
- **`combat_feel_engine.js` lines 119-137**:
  ```javascript
  triggerHitStop(frames = 3, durationSec = 0.05) {
    const duration = Math.max(0.016 * frames, durationSec);
    this.hitStopTimer = Math.max(this.hitStopTimer, duration);
    this.hitStopTotal = this.hitStopTimer;
  }

  isHitStopped(dt) {
    if (this.hitStopTimer > 0) {
      this.hitStopTimer -= dt;
      if (this.hitStopTimer < 0) this.hitStopTimer = 0;
      return true;
    }
    return false;
  }
  ```
  *Observed defect*: Calling `isHitStopped()` without arguments (`dt` is `undefined`) results in `this.hitStopTimer -= undefined` $\rightarrow$ `this.hitStopTimer = NaN`. Because `NaN > 0` is `false`, hit-stop becomes permanently disabled.
  *Multi-caller defect*: If multiple systems call `isHitStopped(dt)` within the same frame (e.g., player state machine, monster AI, camera loop), `this.hitStopTimer` is decremented multiple times in a single tick.

- **Weapon Swing Archetype Specs in `client/webapp/js/data/weapon_swing_catalog.js`**:
  - `SLASH` (Sword/Cleave): `hitStopFrames: 3, hitStopDuration: 0.05` ($50\text{ms}$) (lines 18-19).
  - `THRUST` (Spear/Pierce): `hitStopFrames: 2, hitStopDuration: 0.035` ($35\text{ms}$) (lines 40-41).
  - `SLAM` (Hammer/Mace): `hitStopFrames: 4, hitStopDuration: 0.066` ($66\text{ms}$) (lines 62-63).
  - `BOLT` (Sky Lightning): `hitStopFrames: 2, hitStopDuration: 0.035` ($35\text{ms}$) (lines 84-85).

- **Attacker Animation Pausing in `client/webapp/js/engine/animation_engine.js` lines 149-158**:
  ```javascript
  if (animState.hitStopTimer > 0) {
    animState.hitStopTimer -= dt;
    animState.isHitStopped = true;
    animState.kineticPhase = 'hit_stop';
    if (animState.hitStopTimer <= 0) {
      animState.hitStopTimer = 0;
      animState.isHitStopped = false;
    }
    return;
  }
  animState.isHitStopped = false;
  ```
  `animation_engine.js` already supports `animState.hitStopTimer`, but `combat_skills.js` does not set `player.anim.hitStopTimer` or `target.anim.hitStopTimer` on impact.

### 1.3 Screen Shake & Camera Clamping
- **`combat_feel_engine.js` lines 151-169**:
  ```javascript
  triggerScreenShake(intensity = 4.0, durationSec = 0.2) {
    this.screenShakeIntensity = Math.max(this.screenShakeIntensity, intensity);
    this.screenShakeDuration = Math.max(this.screenShakeDuration, durationSec);
    this.screenShakeTimer = this.screenShakeDuration;
  }
  ```
  *Observed deviation*: There is no upper bound clamping. Extreme damage values can cause uncontrolled shake.
- **E2E Test Assertion in `tests/e2e/test_poe2_ui_animation_vfx_e2e.py` lines 251-255**:
  ```python
  def test_boundary_extreme_screen_shake_clamping(self) -> None:
      """Screen shake intensity from massive damage ($10^6$) clamps to max 8.5."""
      raw_intensity = min(8.5, max(2.5, math.log10(1_000_000.0) * 1.5))
      assert 2.5 <= raw_intensity <= 8.5
  ```
  Upper bound must be clamped to $8.5$, and standard combat shake duration must range between $0.12\text{s}$ and $0.45\text{s}$.

- **Decoupled Camera Shake in `canvas_renderer.js` lines 270-278**:
  ```javascript
  ctx.save();
  if (screenShake.timer > 0) {
    screenShake.timer -= dt;
    const curInt = screenShake.intensity * (screenShake.timer / screenShake.duration);
    const shkX = (Math.random() - 0.5) * 2 * curInt;
    const shkY = (Math.random() - 0.5) * 2 * curInt;
    ctx.translate(shkX, shkY);
  }
  ```
  `canvas_renderer.js` reads from the global `const screenShake` declared in `iso_math.js:237`. `combatFeelEngine.triggerScreenShake()` does not synchronize with `screenShake`, meaning calls to `combatFeelEngine` alone do not move the canvas in `canvas_renderer.js`.

### 1.4 Dynamic Allocation vs Ring Buffer in Live Loop
- In `iso_math.js` lines 330-342:
  ```javascript
  const damageTexts = [];
  function spawnDamageText(text, x, y, color = '#fbbf24', isCrit = false) {
    damageTexts.push({ ... });
  }
  ```
- In `vfx_renderer.js` lines 241-246:
  ```javascript
  for (let i = damageTexts.length - 1; i >= 0; i--) {
    const dtObj = damageTexts[i];
    dtObj.y += dtObj.vy;
    dtObj.alpha -= dt * 1.5;
    if (dtObj.alpha <= 0) { damageTexts.splice(i, 1); continue; }
  ```
  `damageTexts.push()` and `splice()` allocate dynamic heap objects and cause array element shifting on every frame, violating the 120 FPS ProMotion zero-allocation mandate. `combatFeelEngine.damagePool` (pre-allocated 64 slots) is unused.

### 1.5 Test Suite and Hygiene Verification
- `pytest tests/e2e/test_poe2_ui_animation_vfx_e2e.py -v`:
  - `33 passed, 9 xpassed` in 0.36s.
  - `test_f07_hit_stop_engine_baseline` currently XPASSes because the file exists.
  - `test_f16_screen_shake_in_engine` passes because `triggerScreenShake` and `screenShake` are present in `client_bundle`.
- `python tools/lint/check_code_and_doc_hygiene.py --strict`:
  - Total 522 files scanned. 0 Hard Cap violations.
  - `combat_feel_engine.js` is 198 lines (well below the $\le 350$ lines soft cap).

---

## 2. Logic Chain

```
[Observation 1.1: combat_feel_engine.js lines 52-58 (scale 1.4->1.0)]
  ──► Contract requires pop-in bounce scale (1.6 -> 1.0)
  ──► Initial slot scale for normal hits must be 1.6, decaying to 1.0 at progress >= 0.2.
  ──► Crit scale starts at 2.0 and decays to 1.2 at progress >= 0.2.

[Observation 1.2: isHitStopped(dt) with undefined dt produces NaN]
  ──► When isHitStopped() is queried without dt (or dt=0), hitStopTimer becomes NaN.
  ──► Calling isHitStopped(dt) from multiple actors double-decrements hitStopTimer.
  ──► hitStopTimer decrement must only occur if dt is a valid positive number,
      or be decoupled into update(dt) while isHitStopped() acts as a pure state check.

[Observation 1.2 & Catalog: hitStopFrames 2-4 frames / 33-66ms]
  ──► triggerHitStop(frames, durationSec) must compute duration = max((frames/60), durationSec).
  ──► On impact in combat_skills.js:hitMonster, both attacker (player.anim) and
      target (target.anim, target) must receive hitStopTimer = duration.

[Observation 1.3: screenShake intensity unbound; test asserts 2.5-8.5 clamping]
  ──► triggerScreenShake(intensity, durationSec, dirAngle) must clamp intensity:
      clampedInt = Math.min(8.5, Math.max(0.0, intensity));
      clampedDur = Math.min(0.45, Math.max(0.05, durationSec));
  ──► Directional shake matrix must project displacement along strike direction vector
      (cos θ, sin θ) with perpendicular jitter.
  ──► Must sync with screenShake in iso_math.js so canvas_renderer.js renders shake immediately.

[Observation 1.4: damageTexts.push/splice heap churn in vfx_renderer.js]
  ──► Ring Buffer DamageNumberPool (64 slots) in combat_feel_engine.js eliminates all GC churn.
  ──► spawnDamageText in iso_math.js must forward to combatFeelEngine.spawnDamageNumber().
  ──► combatFeelEngine.update(dt) and combatFeelEngine.renderDamageNumbers(ctx) must be
      wired into canvas_renderer.js.
```

---

## 3. Caveats
1. **Network Latency & Server Authorization**: FreeExile's combat feel (hit-stop, camera shake, floating numbers) is client-authoritative for immediate responsiveness ($0\text{ms}$ tactile feel). Server `CombatEngine` (`server/world/combat_engine.py`) authoritative damage calculation is unaffected by client visual hit-stop.
2. **Multiple Simultaneous Hits**: When hitting multiple enemies with an AoE slash or lightning fork, `triggerHitStop` must use `Math.max(this.hitStopTimer, duration)` rather than accumulating durations, preventing excessive freezing.
3. **Backward Compatibility**: Existing functions `window.triggerScreenShake`, `window.triggerHitStop`, and `window.spawnDamageNumber` must remain exposed globally on `window` for legacy systems.

---

## 4. Conclusion & Concrete Strategy for Worker

### 4.1 Required Enhancements in `client/webapp/js/engine/combat_feel_engine.js`

```javascript
/**
 * FreeExile Visceral Combat Feel Engine
 * Provides:
 * 1. Micro hit-stop freeze frames (2-4 frames / 33-66ms pause) on attacker and target
 * 2. Static Ring Buffer DamageNumberPool (64 slots) with 1.6x -> 1.0x pop-in bounce
 * 3. Directional screen shake matrix & camera impact impulse (2.5 - 8.5 intensity, 0.12 - 0.45s)
 */

const MAX_DAMAGE_NUMBERS = 64;

class DamageNumberSlot {
  constructor() {
    this.active = false;
    this.text = '';
    this.x = 0;
    this.y = 0;
    this.startY = 0;
    this.color = '#ffffff';
    this.isCrit = false;
    this.age = 0;
    this.lifetime = 0.75;
    this.scale = 1.0;
    this.alpha = 1.0;
  }

  init(text, x, y, color = '#ffffff', isCrit = false) {
    this.active = true;
    this.text = String(text);
    this.x = x + (Math.random() - 0.5) * 16;
    this.y = y - 10;
    this.startY = this.y;
    this.color = color;
    this.isCrit = isCrit;
    this.age = 0;
    this.lifetime = isCrit ? 0.95 : 0.70;
    this.scale = isCrit ? 2.0 : 1.6;
    this.alpha = 1.0;
  }

  update(dt) {
    if (!this.active) return;
    this.age += dt;
    if (this.age >= this.lifetime) {
      this.active = false;
      return;
    }

    const progress = this.age / this.lifetime;
    // Upward deceleration drift
    this.y = this.startY - Math.sin(progress * Math.PI * 0.5) * (this.isCrit ? 44 : 30);

    // Pop-in bounce scale (1.6x -> 1.0x for normal, 2.0x -> 1.2x for crit in first 20% of lifetime)
    if (progress < 0.2) {
      const p = progress / 0.2;
      this.scale = (this.isCrit ? 2.0 : 1.6) - p * (this.isCrit ? 0.8 : 0.6);
    } else {
      this.scale = this.isCrit ? 1.2 : 1.0;
    }

    // Alpha fadeout in final 35% of lifetime
    if (progress > 0.65) {
      this.alpha = Math.max(0, 1.0 - (progress - 0.65) / 0.35);
    } else {
      this.alpha = 1.0;
    }
  }

  render(ctx) {
    if (!this.active || this.alpha <= 0) return;
    ctx.save();
    ctx.translate(this.x, this.y);
    ctx.scale(this.scale, this.scale);
    ctx.globalAlpha = this.alpha;

    ctx.font = this.isCrit
      ? '900 18px -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif'
      : '700 14px -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif';
    ctx.textAlign = 'center';
    ctx.textBaseline = 'middle';

    // Deep shadow outline for high contrast
    ctx.strokeStyle = 'rgba(0, 0, 0, 0.95)';
    ctx.lineWidth = this.isCrit ? 4.5 : 3.0;
    ctx.strokeText(this.text, 0, 0);

    ctx.fillStyle = this.color;
    ctx.fillText(this.text, 0, 0);

    if (this.isCrit) {
      ctx.shadowColor = this.color;
      ctx.shadowBlur = 12;
      ctx.fillText(this.text, 0, 0);
    }

    ctx.restore();
  }
}

export class CombatFeelEngine {
  constructor() {
    this.hitStopTimer = 0;
    this.hitStopTotal = 0;
    this.screenShakeIntensity = 0;
    this.screenShakeTimer = 0;
    this.screenShakeDuration = 0;
    this.shakeAngle = null;
    this.shakeOffsetX = 0;
    this.shakeOffsetY = 0;

    // Static Pre-allocated Ring Buffer Pool (64 slots)
    this.damagePool = Array.from({ length: MAX_DAMAGE_NUMBERS }, () => new DamageNumberSlot());
    this.poolHead = 0;
  }

  /**
   * Triggers micro hit-stop freeze on impact
   * @param {number} frames - Number of frames to freeze (typically 2-4 / 33-66ms)
   * @param {number} durationSec - Duration in seconds
   * @param {Object} [attacker] - Optional attacker entity/animState to freeze
   * @param {Object} [target] - Optional target entity/animState to freeze
   */
  triggerHitStop(frames = 3, durationSec = 0.05, attacker = null, target = null) {
    const duration = Math.max(frames * 0.0166, durationSec);
    this.hitStopTimer = Math.max(this.hitStopTimer, duration);
    this.hitStopTotal = this.hitStopTimer;

    if (attacker && attacker.anim) {
      attacker.anim.hitStopTimer = Math.max(attacker.anim.hitStopTimer || 0, duration);
    }
    if (target) {
      if (target.anim) target.anim.hitStopTimer = Math.max(target.anim.hitStopTimer || 0, duration);
      target.hitStopTimer = Math.max(target.hitStopTimer || 0, duration);
    }
  }

  /**
   * Checks whether the engine is currently in hit-stop freeze
   * @param {number} [dt] - Optional delta time in seconds
   * @returns {boolean} true if engine should pause frame advancement
   */
  isHitStopped(dt = 0) {
    if (this.hitStopTimer > 0) {
      if (typeof dt === 'number' && dt > 0) {
        this.hitStopTimer = Math.max(0, this.hitStopTimer - dt);
      }
      return true;
    }
    return false;
  }

  /**
   * Spawns a floating damage number from the pre-allocated ring buffer
   */
  spawnDamageNumber(text, x, y, color = '#ffffff', isCrit = false) {
    const slot = this.damagePool[this.poolHead];
    slot.init(text, x, y, color, isCrit);
    this.poolHead = (this.poolHead + 1) % MAX_DAMAGE_NUMBERS;
  }

  /**
   * Triggers directional camera screen shake
   * @param {number} intensity - Screen shake intensity (clamped 2.5 - 8.5)
   * @param {number} durationSec - Duration in seconds (clamped 0.12 - 0.45s)
   * @param {number|null} [dirAngle] - Optional strike angle in radians for directional shake
   */
  triggerScreenShake(intensity = 4.0, durationSec = 0.2, dirAngle = null) {
    const clampedInt = Math.min(8.5, Math.max(0.0, intensity));
    const clampedDur = Math.min(0.45, Math.max(0.05, durationSec));
    this.screenShakeIntensity = Math.max(this.screenShakeIntensity, clampedInt);
    this.screenShakeDuration = Math.max(this.screenShakeDuration, clampedDur);
    this.screenShakeTimer = this.screenShakeDuration;
    this.shakeAngle = dirAngle;

    // Backward compatibility sync with iso_math.js screenShake
    if (typeof screenShake !== 'undefined') {
      screenShake.intensity = this.screenShakeIntensity;
      screenShake.duration = this.screenShakeDuration;
      screenShake.timer = this.screenShakeTimer;
    }
  }

  /**
   * Returns current 2D shake offset matrix values
   */
  getShakeOffset() {
    return { x: this.shakeOffsetX, y: this.shakeOffsetY };
  }

  update(dt) {
    // Update screen shake decay
    if (this.screenShakeTimer > 0) {
      this.screenShakeTimer -= dt;
      const decay = Math.max(0, this.screenShakeTimer / this.screenShakeDuration);
      const curIntensity = this.screenShakeIntensity * decay;

      if (this.shakeAngle !== null && this.shakeAngle !== undefined) {
        // Directional shake: oscillation along impact vector + orthogonal jitter
        const osc = Math.sin(this.screenShakeTimer * 45.0);
        const perp = (Math.random() - 0.5) * 0.4;
        const mainX = Math.cos(this.shakeAngle) * osc * curIntensity;
        const mainY = Math.sin(this.shakeAngle) * osc * curIntensity;
        const perpX = -Math.sin(this.shakeAngle) * perp * curIntensity;
        const perpY = Math.cos(this.shakeAngle) * perp * curIntensity;
        this.shakeOffsetX = mainX + perpX;
        this.shakeOffsetY = mainY + perpY;
      } else {
        // Isotropic shake
        this.shakeOffsetX = (Math.random() - 0.5) * 2 * curIntensity;
        this.shakeOffsetY = (Math.random() - 0.5) * 2 * curIntensity;
      }
    } else {
      this.shakeOffsetX = 0;
      this.shakeOffsetY = 0;
      this.screenShakeIntensity = 0;
      this.shakeAngle = null;
    }

    // Update active damage numbers
    for (let i = 0; i < MAX_DAMAGE_NUMBERS; i++) {
      if (this.damagePool[i].active) {
        this.damagePool[i].update(dt);
      }
    }
  }

  renderDamageNumbers(ctx) {
    for (let i = 0; i < MAX_DAMAGE_NUMBERS; i++) {
      if (this.damagePool[i].active) {
        this.damagePool[i].render(ctx);
      }
    }
  }
}

export const combatFeelEngine = new CombatFeelEngine();

if (typeof window !== 'undefined') {
  window.CombatFeelEngine = CombatFeelEngine;
  window.combatFeelEngine = combatFeelEngine;
  window.triggerHitStop = (frames, duration, attacker, target) =>
    combatFeelEngine.triggerHitStop(frames, duration, attacker, target);
  window.triggerScreenShake = (intensity, duration, dirAngle) =>
    combatFeelEngine.triggerScreenShake(intensity, duration, dirAngle);
  window.spawnDamageNumber = (text, x, y, color, isCrit) =>
    combatFeelEngine.spawnDamageNumber(text, x, y, color, isCrit);
}
```

### 4.2 Integration Touchpoints for Worker
1. **In `client/webapp/js/engine/combat_skills.js`**:
   - In `hitMonster(elem, dmg, isCrit, explicitTarget)`:
     ```javascript
     const hitFrames = isCrit ? 4 : 3;
     const hitDuration = isCrit ? 0.066 : 0.05;
     if (typeof window.triggerHitStop === 'function') {
       window.triggerHitStop(hitFrames, hitDuration, player, target);
     }
     ```
2. **In `client/webapp/js/engine/canvas_renderer.js`**:
   - In `renderLoop(now)`:
     ```javascript
     if (typeof window.combatFeelEngine !== 'undefined') {
       window.combatFeelEngine.update(dt);
     }
     ```
   - In camera shake section:
     ```javascript
     ctx.save();
     if (typeof window.combatFeelEngine !== 'undefined' && (window.combatFeelEngine.shakeOffsetX !== 0 || window.combatFeelEngine.shakeOffsetY !== 0)) {
       ctx.translate(window.combatFeelEngine.shakeOffsetX, window.combatFeelEngine.shakeOffsetY);
     } else if (typeof screenShake !== 'undefined' && screenShake.timer > 0) {
       ...
     }
     ```
   - In entity / VFX render phase:
     ```javascript
     if (typeof window.combatFeelEngine !== 'undefined') {
       window.combatFeelEngine.renderDamageNumbers(ctx);
     }
     ```
3. **In `client/webapp/js/engine/iso_math.js`**:
   - Forward `spawnDamageText` to `window.spawnDamageNumber`:
     ```javascript
     function spawnDamageText(text, x, y, color = '#fbbf24', isCrit = false) {
       if (typeof window !== 'undefined' && typeof window.spawnDamageNumber === 'function') {
         window.spawnDamageNumber(text, x, y, color, isCrit);
         return;
       }
       damageTexts.push({ ... });
     }
     ```

### 4.3 Dedicated Unit Test Suite: `tests/unit/test_combat_feel_engine.py`
The worker should implement a comprehensive Python unit test covering:
1. `test_hit_stop_timer_and_frames_calculation` (2 frames = 33ms, 3 frames = 50ms, 4 frames = 66ms).
2. `test_is_hit_stopped_safe_without_dt_argument` (zero `NaN` corruption).
3. `test_screen_shake_clamping_and_directional_bias` ($2.5 - 8.5$ intensity, $0.12 - 0.45\text{s}$ duration).
4. `test_damage_number_pool_ring_buffer_wrapping` (64 slots static allocation, $O(1)$ ring buffer).
5. `test_damage_number_pop_in_bounce_scale` ($1.6 \rightarrow 1.0$ for normal hits, $2.0 \rightarrow 1.2$ for crits).
6. `test_zero_heap_allocation_in_hot_path` (verification that no dynamic arrays are grown).

---

## 5. Verification Method

To independently verify these findings and confirm the system state:

1. **Hygiene Audit Gate**:
   ```bash
   python tools/lint/check_code_and_doc_hygiene.py --strict
   ```
   *Expected outcome*: Exit code 0, 0 hard cap violations. `combat_feel_engine.js` remains $\le 250$ lines (Soft cap: 350).

2. **Existing E2E Test Suite**:
   ```bash
   pytest tests/e2e/test_poe2_ui_animation_vfx_e2e.py -v
   ```
   *Expected outcome*: 33 passed, 9 xpassed. Specifically:
   - `test_f07_hit_stop_engine_baseline`
   - `test_f16_screen_shake_in_engine`
   - `test_cross_hit_stop_during_screen_shake`
   - `test_boundary_extreme_screen_shake_clamping`
   - `test_scenario_120fps_promotion_budget_breakdown`

3. **Character Animation & VFX Unit Test Suite**:
   ```bash
   python -m unittest tests/unit/test_character_animation_and_skills_vfx.py
   ```
   *Expected outcome*: Ran 10 tests, OK.

4. **Dedicated Combat Feel Engine Unit Tests**:
   After implementation by the worker:
   ```bash
   python -m unittest tests/unit/test_combat_feel_engine.py
   ```
   *Expected outcome*: All test cases pass 100%.

5. **Invalidation Conditions**:
   - If `combat_feel_engine.js` line count exceeds 350 lines, it must be modularized.
   - If `isHitStopped()` returns `false` or causes `NaN` when called without arguments, the fix is invalid.
   - If screen shake intensity exceeds 8.5 under extreme inputs, the boundary guard is invalid.
