# Project: FreeExile Character Stat Aggregator System

## Architecture
- **Stat Core & Types** (`server/stats/stat_types.py` & `server/stats/stat_aggregator.py`):
  - Pure mathematical aggregator implementing the canonical Path of Exile formula:
    $$\text{FinalStat} = (\text{Base} + \sum \text{Flat}) \times (1.0 + \frac{\sum \text{Inc} - \sum \text{Red}}{100.0}) \times \prod (1.0 + \text{More}) \times \prod (1.0 - \text{Less})$$
  - Ingests:
    1. Base Character Attributes (`CharacterLoadout` in `martial_types.py` / default 50 STR/DEX/INT, 1000 HP, 500 Qi, 50.0 base attack).
    2. Equipped Item Affixes (`CharacterInventory.equipment` in `inventory_service.py`), supporting both `Affix` (`primal_stones_crafting.py`) and `AffixMod` (`item_crafting_types.py`), plus Two-Handed (+50% More damage) and Dual Wielding (+10% APS, +15% Block) mechanics.
    3. Meridian Passive Tree stats (`MeridianService.compute_total_stats(player_id)` producing `MeridianStatBonus` across 29 acupoints and 4 socketed jewels).
  - Evaluates:
    1. Tag-based filters (`SkillTag` e.g. `FIRE`, `MELEE`, `SPELL`, `PROJECTILE`).
    2. Conditional modifiers (e.g. `ON_LOW_HEALTH` when $HP \le 35\%$, `AGAINST_BLEEDING`, `WIELDING_SWORD`).
- **Formula Persistence Engine** (`server/stats/formula_persistence.py`):
  - Serializes calculation steps and AST hierarchy (`Constant`, `Sum`, `ScaleFactor`, `Product`, `ModifierContribution`) into JSON.
  - Persists records into SQLite database `data/character_stat_formulas.db` (table: `character_stat_calculations`) using WAL mode, row factory `sqlite3.Row`, and in-memory (`:memory:`) support for unit tests.
- **Server Engine Loop Integration** (`server/world/server_engine_loop.py`):
  - Requires pre-flight blast radius acknowledgment: `python tools/analysis/blast_radius.py --target server/world/server_engine_loop.py --ack`.
  - Injects `stat_aggregator: Optional[CharacterStatAggregator] = None` into `ServerEngineLoop.__init__`.
  - Updates `register_player` to calculate/accept aggregated stats, instantiating `CombatActor` with computed `base_attack`, `max_hp`, `crit_chance`, `crit_multiplier`, `resistances` (mapped to `FiveElements`), `is_player=True`, and synchronizing `PlayerCharacter.move_speed`.
  - Falls back gracefully to legacy defaults (`base_attack=50.0`, `max_hp=1000.0`, `move_speed=6.0`) when `aggregated_stats` is omitted, guaranteeing 100% backward compatibility for all existing tests.

## Feature Inventory
| # | Feature | Description | Milestone | Source |
|---|---------|-------------|-----------|--------|
| 1 | Stat Types & AST Nodes | Define `ModifierType`, `StatModifier`, AST calculation nodes, `EvaluationContext`, and `AggregatedCharacterStats` | M1 | ORIGINAL_REQUEST R1 |
| 2 | PoE Mathematical Pipeline | Implement base + flat, increased/reduced additive pool, more/less multiplicative pool in `CharacterStatAggregator` | M1 | ORIGINAL_REQUEST R1 |
| 3 | Inventory Equipment Ingestion | Extract flat and percentage affixes from `CharacterInventory.equipment` supporting dual affix formats and weapon grips | M1 | ORIGINAL_REQUEST R1 |
| 4 | Meridian Passives Ingestion | Ingest passive node bonuses and socketed jewels from `MeridianService.compute_total_stats` | M1 | ORIGINAL_REQUEST R1 |
| 5 | Tag-Based Filtering | Filter modifiers by matching modifier tags against query context tags (e.g. fire, melee, spell) | M1 | ORIGINAL_REQUEST R1 |
| 6 | Conditional Modifier Evaluation | Evaluate conditions (e.g. low life, wielding specific weapon) before applying modifiers | M1 | ORIGINAL_REQUEST R1 |
| 7 | Formula AST & JSON Serialization | Construct AST tree for each calculated stat representing every mathematical step | M2 | ORIGINAL_REQUEST R2 |
| 8 | SQLite Persistence Layer | Create `character_stat_calculations` table in SQLite (`data/character_stat_formulas.db`) and persist calculation JSON | M2 | ORIGINAL_REQUEST R2 |
| 9 | Blast Radius Gate Acknowledgment | Run `tools/analysis/blast_radius.py --target server/world/server_engine_loop.py --ack` prior to editing engine loop | M3 | GEMINI.md Directives |
| 10 | Server Engine Loop Wiring | Update `ServerEngineLoop.__init__` and `register_player` to instantiate `CombatActor` with aggregated stats | M3 | ORIGINAL_REQUEST R3 |
| 11 | Movement Speed Authority Sync | Synchronize aggregated movement speed to `PlayerCharacter.move_speed` for anti-speedhack compatibility | M3 | ORIGINAL_REQUEST R3 |
| 12 | Backward Compatibility Fallback | Preserve default `50.0` attack and `1000.0` HP when `aggregated_stats` is omitted in `register_player` | M3 | ORIGINAL_REQUEST R3 |
| 13 | Comprehensive Unit Test Suite | Write `tests/unit/test_character_stat_aggregator.py` covering all features, math assertions, and persistence verification | M4 | ORIGINAL_REQUEST AC |
| 14 | Code & Doc Hygiene Compliance | Ensure all files remain <= 350 soft cap / <= 500 hard cap, pass `check_code_and_doc_hygiene.py --strict` | M4 | GEMINI.md Directives |

## Milestones
| # | Name | Scope | Dependencies | Status |
|---|------|-------|-------------|--------|
| M1 | Stat Aggregator Core & Domain Types | `server/stats/stat_types.py`, `server/stats/stat_aggregator.py` | none | DONE |
| M2 | Formula Persistence SQLite Engine | `server/stats/formula_persistence.py`, `data/character_stat_formulas.db` | M1 | DONE |
| M3 | Server Engine Loop Integration | `server/world/server_engine_loop.py` | M1, M2 | DONE |
| M4 | Comprehensive Test Verification | `tests/unit/test_character_stat_aggregator.py` | M1, M2, M3 | DONE |

## Interface Contracts
### `server/stats/stat_types.py`
- `ModifierType(Enum)`: `FLAT = "flat"`, `INCREASED = "increased"`, `REDUCED = "reduced"`, `MORE = "more"`, `LESS = "less"`
- `StatModifier`: `stat_key: str`, `mod_type: ModifierType`, `value: float`, `tags: FrozenSet[str] = frozenset()`, `condition: Optional[str] = None`, `source: str = ""`
- `EvaluationContext`: `active_tags: FrozenSet[str]`, `conditions: FrozenSet[str]`, `current_hp_ratio: float = 1.0`
- `AggregatedCharacterStats`: `attack_damage: float`, `max_hp: float`, `crit_chance: float`, `crit_multiplier: float`, `move_speed: float`, `resistances: Dict[str, float]`, `all_stats: Dict[str, float]`
- `ASTNode`: `ConstantNode`, `SumNode`, `ScaleFactorNode`, `ProductNode`, `ModifierContributionNode`

### `server/stats/formula_persistence.py` ↔ `server/stats/stat_aggregator.py`
- `FormulaPersistenceService(db_path: Optional[str] = None)`
  - `save_calculation(player_id: str, calculation_id: str, formula_ast: Dict[str, Any], final_stats: Dict[str, float], context_tags: List[str]) -> int`
  - `get_calculation(calculation_id: str) -> Optional[Dict[str, Any]]`
  - `get_player_calculations(player_id: str, limit: int = 10) -> List[Dict[str, Any]]`

### `server/stats/stat_aggregator.py` ↔ `server/world/server_engine_loop.py`
- `CharacterStatAggregator(persistence_service: Optional[FormulaPersistenceService] = None)`
  - `calculate_stats(character_loadout: Optional[Any] = None, inventory: Optional[Any] = None, meridian_bonus: Optional[Any] = None, context: Optional[EvaluationContext] = None, player_id: Optional[str] = None) -> AggregatedCharacterStats`
- `ServerEngineLoop.register_player(entity_id: int, initial_x: float = 0.0, initial_y: float = 0.0, element: FiveElements = FiveElements.METAL, player_id: Optional[str] = None, account_id: Optional[str] = None, character_id: Optional[str] = None, aggregated_stats: Optional[AggregatedCharacterStats] = None, context: Optional[EvaluationContext] = None) -> CombatActor`

## Code Layout
- `server/stats/`:
  - `__init__.py`: Package export
  - `stat_types.py`: Types, enums, AST nodes, EvaluationContext, AggregatedCharacterStats (< 200 lines)
  - `formula_persistence.py`: SQLite persistence service, schema, query/insert functions (< 250 lines)
  - `stat_aggregator.py`: `CharacterStatAggregator` core calculations and adapter logic (< 350 lines)
- `server/world/`:
  - `server_engine_loop.py`: Engine loop integration (under `--ack` protection)
- `tests/unit/`:
  - `test_character_stat_aggregator.py`: Comprehensive TDD unit test suite
