# Ability JSON Schema Documentation

> This document explains the structure of ability definition files used in the system.
> It is intended for **wiki contributors** and **tool developers** alike.

---

## Top-Level Structure

Every ability file follows this wrapper:

```json
{
  "abilities": {
    "id": "namespace:category/name",
    "displayName": "translation.key.for.name",
    "description": "translation.key.for.description",
    "math": [ ...nodes ]
  }
}
```

| Field         | Type   | Required | Description |
|---------------|--------|----------|-------------|
| `id`          | string | ✅        | Unique ID in `namespace:category/name` format |
| `displayName` | string | ✅        | i18n translation key for the display name |
| `description` | string | ✅        | i18n translation key for the tooltip/description |
| `math`        | array  | ✅        | Ordered list of effect nodes (see below) |

---

## The `id` Format

IDs follow the pattern: `namespace:category/name`

| Part        | Example       | Notes |
|-------------|---------------|-------|
| `namespace` | `original`    | The mod or source that owns this ability |
| `category`  | `fire`        | The element, school, or grouping |
| `name`      | `fireball`    | The specific ability |

**Examples:**
- `original:fire/fireball`
- `original:ice/frost_bolt`
- `mymod:arcane/void_lance`

---

## Translation Keys

`displayName` and `description` are **not raw text** — they are keys looked up in a translation file.

**Convention:** `ability.<namespace>.<name>.<field>`

| Field         | Example Key                          |
|---------------|--------------------------------------|
| `displayName` | `ability.original.fireball.name`     |
| `description` | `ability.original.fireball.description` |

---

## Math Nodes

The `math` array is the heart of an ability. Each node is an object with at minimum:

```json
{ "id": "unique_node_id", "type": "node_type", ...fields }
```

| Field  | Type   | Required | Description |
|--------|--------|----------|-------------|
| `id`   | string | ✅        | Unique name for this node within the ability |
| `type` | string | ✅        | Determines what this node does (see types below) |

---

## Node Types

---

### `base_value`
The foundational damage or healing number, before any modifiers.

```json
{
  "id": "base_damage",
  "type": "base_value",
  "amount": 50,
  "scaling": { "stat": "spell_power", "multiplier": 1.5 }
}
```

| Field              | Type   | Required | Description |
|--------------------|--------|----------|-------------|
| `amount`           | number | ✅        | The flat base value |
| `scaling.stat`     | string | ✅        | Which player stat to scale from |
| `scaling.multiplier` | number | ✅     | How much of that stat to add (`stat × multiplier`) |

> **Formula:** `final = amount + (player[stat] × multiplier)`

---

### `range`
How far the ability can reach and how it travels.

```json
{
  "id": "cast_range",
  "type": "range",
  "min": 0,
  "max": 30,
  "unit": "meters",
  "rangeType": "projectile"
}
```

| Field       | Type   | Required | Description |
|-------------|--------|----------|-------------|
| `min`       | number | ✅        | Minimum range (use `> 0` for dead zones) |
| `max`       | number | ✅        | Maximum range (`null` = unlimited) |
| `unit`      | string | ✅        | `"meters"` |
| `rangeType` | string | ✅        | `projectile`, `hitscan`, `melee`, `aura` |

---

### `area_of_effect`
Defines the shape and spread of the ability's impact zone.

```json
{
  "id": "explosion",
  "type": "area_of_effect",
  "shape": "sphere",
  "radius": 5,
  "unit": "meters",
  "falloff": "linear"
}
```

| Field     | Type   | Required | Description |
|-----------|--------|----------|-------------|
| `shape`   | string | ✅        | `sphere`, `cone`, `cylinder`, `line` |
| `radius`  | number | ✅        | Size of the AoE |
| `unit`    | string | ✅        | `"meters"` |
| `falloff` | string | ✅        | `none`, `linear`, `quadratic` — how damage drops off at edges |

---

### `damage`
Instant damage dealt. Supports **multiple damage types** in one node via `sources`.

```json
{
  "id": "instant_damage",
  "type": "damage",
  "sources": [
    {
      "damageType": "fire",
      "base_value": 50,
      "scaling": { "stat": "spell_power", "multiplier": 1.5 }
    },
    {
      "damageType": "physical",
      "base_value": 15,
      "scaling": { "stat": "strength", "multiplier": 0.5 }
    }
  ]
}
```

| Field                    | Type   | Required | Description |
|--------------------------|--------|----------|-------------|
| `sources`                | array  | ✅        | One entry per damage type |
| `sources[].damageType`   | string | ✅        | e.g. `fire`, `physical`, `lightning`, `poison` |
| `sources[].base_value`   | number | ✅        | Flat damage for this type |
| `sources[].scaling`      | object | ✅        | Same `stat` / `multiplier` structure as `base_value` |

> **Note:** Multiple sources are applied **independently** — each scales off its own stat.

---

### `damage_over_time`
Repeating damage applied in ticks after the initial hit.

```json
{
  "id": "burn_dot",
  "type": "damage_over_time",
  "damageType": "fire",
  "damage_per_tick": 10,
  "tick_interval_seconds": 1,
  "duration_seconds": 5,
  "scaling": { "stat": "spell_power", "multiplier": 0.3 },
  "stacks": false
}
```

| Field                   | Type    | Required | Description |
|-------------------------|---------|----------|-------------|
| `damageType`            | string  | ✅        | Damage type per tick |
| `damage_per_tick`       | number  | ✅        | Flat damage each tick |
| `tick_interval_seconds` | number  | ✅        | Seconds between ticks |
| `duration_seconds`      | number  | ✅        | Total duration |
| `scaling`               | object  | ✅        | Same `stat` / `multiplier` structure |
| `stacks`                | boolean | ✅        | `true` = re-applying adds a new stack; `false` = resets timer |

> **Total ticks:** `duration_seconds / tick_interval_seconds`

---

### `condition`
A status effect or triggered reaction applied to the target.

```json
{
  "id": "ignite_debuff",
  "type": "condition",
  "chance": 0.75,
  "duration_seconds": 5,
  "effect": "reduce_fire_resistance",
  "magnitude": -20
}
```

```json
{
  "id": "explosion_knockback",
  "type": "condition",
  "chance": 1.0,
  "effect": "knockback",
  "force": 8,
  "direction": "away_from_origin"
}
```

| Field              | Type   | Required | Description |
|--------------------|--------|----------|-------------|
| `chance`           | number | ✅        | Probability `0.0`–`1.0` (`1.0` = always) |
| `effect`           | string | ✅        | The condition to apply — mapped by the engine |
| `duration_seconds` | number | ❌        | How long the condition lasts (omit for instant effects) |
| `magnitude`        | number | ❌        | Numeric modifier for stat-changing effects |
| `force`            | number | ❌        | For displacement effects like knockback |
| `direction`        | string | ❌        | `away_from_origin`, `toward_origin`, `up` |

> Multiple `condition` nodes are rolled **independently** per hit.

---

### `meta`
Gameplay configuration — cooldowns, costs, and tags.

```json
{
  "id": "fireball_meta",
  "type": "meta",
  "cooldown_seconds": 12,
  "mana_cost": 80,
  "cast_time_seconds": 1.5,
  "tags": ["fire", "aoe", "projectile", "dot"]
}
```

| Field                | Type     | Required | Description |
|----------------------|----------|----------|-------------|
| `cooldown_seconds`   | number   | ✅        | Recharge time after use |
| `mana_cost`          | number   | ✅        | Resource cost to cast |
| `cast_time_seconds`  | number   | ✅        | Time before the ability fires (`0` = instant) |
| `tags`               | string[] | ✅        | Used for filtering, synergies, and resistances |

---

## Complete Example — Fireball

```json
{
  "abilities": {
    "id": "original:fire/fireball",
    "displayName": "ability.original.fireball.name",
    "description": "ability.original.fireball.description",
    "math": [
      {
        "id": "base_damage",
        "type": "base_value",
        "amount": 50,
        "scaling": { "stat": "spell_power", "multiplier": 1.5 }
      },
      {
        "id": "cast_range",
        "type": "range",
        "min": 0,
        "max": 30,
        "unit": "meters",
        "rangeType": "projectile"
      },
      {
        "id": "explosion",
        "type": "area_of_effect",
        "shape": "sphere",
        "radius": 5,
        "unit": "meters",
        "falloff": "linear"
      },
      {
        "id": "instant_damage",
        "type": "damage",
        "sources": [
          {
            "damageType": "fire",
            "base_value": 50,
            "scaling": { "stat": "spell_power", "multiplier": 1.5 }
          },
          {
            "damageType": "physical",
            "base_value": 15,
            "scaling": { "stat": "strength", "multiplier": 0.5 }
          }
        ]
      },
      {
        "id": "burn_dot",
        "type": "damage_over_time",
        "damageType": "fire",
        "damage_per_tick": 10,
        "tick_interval_seconds": 1,
        "duration_seconds": 5,
        "scaling": { "stat": "spell_power", "multiplier": 0.3 },
        "stacks": false
      },
      {
        "id": "ignite_debuff",
        "type": "condition",
        "chance": 0.75,
        "duration_seconds": 5,
        "effect": "reduce_fire_resistance",
        "magnitude": -20
      },
      {
        "id": "explosion_knockback",
        "type": "condition",
        "chance": 1.0,
        "effect": "knockback",
        "force": 8,
        "direction": "away_from_origin"
      },
      {
        "id": "fireball_meta",
        "type": "meta",
        "cooldown_seconds": 12,
        "mana_cost": 80,
        "cast_time_seconds": 1.5,
        "tags": ["fire", "aoe", "projectile", "dot"]
      }
    ]
  }
}
```

---

## Quick Reference Card

| Node Type         | Purpose                             | Key Fields |
|-------------------|-------------------------------------|------------|
| `base_value`      | Core damage number + stat scaling   | `amount`, `scaling` |
| `range`           | How far & how the ability travels   | `min`, `max`, `rangeType` |
| `area_of_effect`  | Shape & spread of impact zone       | `shape`, `radius`, `falloff` |
| `damage`          | Instant hit damage (multi-type ok)  | `sources[]` |
| `damage_over_time`| Tick damage over time               | `damage_per_tick`, `tick_interval_seconds`, `duration_seconds`, `stacks` |
| `condition`       | Status effects & reactions          | `chance`, `effect`, `duration_seconds` |
| `meta`            | Cooldown, cost, cast time, tags     | `cooldown_seconds`, `mana_cost`, `cast_time_seconds`, `tags` |

---

## Rules & Conventions

1. Every ability **must** have at least one `meta` node.
2. `id` values must be **unique within the same ability file** — use descriptive names.
3. `displayName` and `description` must be **translation keys**, never raw text.
4. The `math` array is processed **top to bottom** — order matters for dependent effects.
5. `damage` nodes should come **after** any `area_of_effect` nodes that modify their area.
6. A `damage_over_time` node is **separate** from `damage` — both can coexist freely.
7. Multiple `condition` nodes are each rolled independently.
8. `tags` on the `meta` node are used by the engine for resistance checks, talent synergies, and UI filtering.
