From ff4db86c967caa8512096811bfab69b577f7beab Mon Sep 17 00:00:00 2001 From: Robert MacRae Date: Sat, 18 Apr 2026 18:58:19 -0300 Subject: [PATCH] proposal on future modular attack system --- ABILITY_SCHEMA.md | 383 ++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 383 insertions(+) create mode 100644 ABILITY_SCHEMA.md diff --git a/ABILITY_SCHEMA.md b/ABILITY_SCHEMA.md new file mode 100644 index 0000000..584da7d --- /dev/null +++ b/ABILITY_SCHEMA.md @@ -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...` + +| 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.