proposal on future modular attack system

This commit is contained in:
Robert MacRae 2026-04-18 18:58:19 -03:00
parent e7f98d1228
commit ff4db86c96

383
ABILITY_SCHEMA.md Normal file
View File

@ -0,0 +1,383 @@
# 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.