{
  "$schema": "https://json-schema.org/draft/2020-12/schema",
  "$id": "urn:locanda:schema:spell",
  "title": "Spell",
  "description": "One SRD 5.2.1 spell in one locale (docs/spec/05-data-model.md). Enum values are lowercase English keys; only text fields differ between locales. Distances are in feet.",
  "type": "object",
  "additionalProperties": false,
  "required": [
    "id",
    "source",
    "name",
    "level",
    "school",
    "classes",
    "castingTime",
    "ritual",
    "range",
    "components",
    "duration",
    "concentration",
    "damageTypes",
    "savingThrows",
    "description",
    "higherLevels",
    "cantripUpgrade"
  ],
  "properties": {
    "id": { "$ref": "#/$defs/spellId" },
    "source": { "const": "SRD 5.2.1" },
    "name": { "$ref": "#/$defs/singleLine" },
    "level": { "$ref": "#/$defs/level" },
    "school": { "$ref": "#/$defs/school" },
    "classes": { "$ref": "#/$defs/classes" },
    "castingTime": { "$ref": "#/$defs/castingTime" },
    "ritual": { "type": "boolean" },
    "range": { "$ref": "#/$defs/range" },
    "components": { "$ref": "#/$defs/components" },
    "duration": { "$ref": "#/$defs/duration" },
    "concentration": { "type": "boolean" },
    "damageTypes": { "$ref": "#/$defs/damageTypes" },
    "savingThrows": { "$ref": "#/$defs/savingThrows" },
    "description": { "$ref": "#/$defs/markdown" },
    "higherLevels": { "oneOf": [{ "$ref": "#/$defs/markdown" }, { "type": "null" }] },
    "cantripUpgrade": { "oneOf": [{ "$ref": "#/$defs/markdown" }, { "type": "null" }] }
  },
  "allOf": [{ "$ref": "#/$defs/levelledText" }],
  "$defs": {
    "spellId": {
      "description": "Kebab-case slug of the English name, same rule as Foundry dnd5e system.identifier.",
      "type": "string",
      "pattern": "^[a-z0-9]+(?:-[a-z0-9]+)*$",
      "maxLength": 100
    },
    "casterClass": {
      "enum": ["bard", "cleric", "druid", "paladin", "ranger", "sorcerer", "warlock", "wizard"]
    },
    "singleLine": {
      "type": "string",
      "minLength": 1,
      "maxLength": 100,
      "pattern": "^[^\\u0000-\\u001F\\u007F]+$"
    },
    "text": {
      "type": "string",
      "minLength": 1,
      "maxLength": 500,
      "pattern": "^[^\\u0000-\\u001F\\u007F]+$"
    },
    "markdown": {
      "description": "Markdown without raw HTML; tabs and newlines allowed, other control characters not.",
      "type": "string",
      "minLength": 1,
      "maxLength": 20000,
      "pattern": "^[^\\u0000-\\u0008\\u000B\\u000C\\u000E-\\u001F\\u007F]+$"
    },
    "level": { "type": "integer", "minimum": 0, "maximum": 9 },
    "school": {
      "enum": [
        "abjuration",
        "conjuration",
        "divination",
        "enchantment",
        "evocation",
        "illusion",
        "necromancy",
        "transmutation"
      ]
    },
    "classes": {
      "type": "array",
      "minItems": 1,
      "uniqueItems": true,
      "items": { "$ref": "#/$defs/casterClass" }
    },
    "castingTime": {
      "type": "object",
      "additionalProperties": false,
      "required": ["unit", "amount", "condition"],
      "properties": {
        "unit": { "enum": ["action", "bonus-action", "reaction", "minute", "hour"] },
        "amount": { "type": "integer", "minimum": 1, "maximum": 1000 },
        "condition": {
          "description": "Reaction trigger or other casting condition, as written in the locale's SRD.",
          "oneOf": [{ "$ref": "#/$defs/text" }, { "type": "null" }]
        }
      }
    },
    "range": {
      "type": "object",
      "additionalProperties": false,
      "required": ["type", "distance", "unit", "area"],
      "properties": {
        "type": { "enum": ["self", "touch", "ranged", "sight", "unlimited", "special"] },
        "distance": { "type": ["number", "null"], "exclusiveMinimum": 0, "maximum": 1000000 },
        "unit": { "enum": ["feet", "miles", null] },
        "area": {
          "oneOf": [
            { "type": "null" },
            {
              "type": "object",
              "additionalProperties": false,
              "required": ["shape", "size", "unit"],
              "properties": {
                "shape": { "enum": ["cone", "cube", "cylinder", "emanation", "line", "sphere"] },
                "size": { "type": "number", "exclusiveMinimum": 0, "maximum": 1000000 },
                "unit": { "enum": ["feet", "miles"] }
              }
            }
          ]
        }
      },
      "if": { "properties": { "type": { "const": "ranged" } } },
      "then": {
        "properties": { "distance": { "type": "number" }, "unit": { "enum": ["feet", "miles"] } }
      },
      "else": { "properties": { "distance": { "type": "null" }, "unit": { "type": "null" } } }
    },
    "components": {
      "type": "object",
      "additionalProperties": false,
      "required": ["v", "s", "m"],
      "properties": {
        "v": { "type": "boolean" },
        "s": { "type": "boolean" },
        "m": {
          "oneOf": [
            { "type": "null" },
            {
              "type": "object",
              "additionalProperties": false,
              "required": ["text", "costGp", "consumed"],
              "properties": {
                "text": { "$ref": "#/$defs/text" },
                "costGp": { "type": ["number", "null"], "minimum": 0, "maximum": 1000000 },
                "consumed": { "type": "boolean" }
              }
            }
          ]
        }
      }
    },
    "duration": {
      "type": "object",
      "additionalProperties": false,
      "required": ["type", "amount", "unit"],
      "properties": {
        "type": { "enum": ["instantaneous", "timed", "until-dispelled", "until-dispelled-or-triggered", "special"] },
        "amount": { "type": ["integer", "null"], "minimum": 1, "maximum": 1000 },
        "unit": { "enum": ["round", "minute", "hour", "day", null] }
      },
      "if": { "properties": { "type": { "const": "timed" } } },
      "then": {
        "properties": { "amount": { "type": "integer" }, "unit": { "enum": ["round", "minute", "hour", "day"] } }
      },
      "else": { "properties": { "amount": { "type": "null" }, "unit": { "type": "null" } } }
    },
    "damageTypes": {
      "type": "array",
      "uniqueItems": true,
      "items": {
        "enum": [
          "acid",
          "bludgeoning",
          "cold",
          "fire",
          "force",
          "lightning",
          "necrotic",
          "piercing",
          "poison",
          "psychic",
          "radiant",
          "slashing",
          "thunder"
        ]
      }
    },
    "savingThrows": {
      "type": "array",
      "uniqueItems": true,
      "items": { "enum": ["str", "dex", "con", "int", "wis", "cha"] }
    },
    "levelledText": {
      "description": "Cantrips have no higher-level text; levelled spells have no cantrip upgrade.",
      "type": "object",
      "if": { "properties": { "level": { "const": 0 } } },
      "then": { "properties": { "higherLevels": { "type": "null" } } },
      "else": { "properties": { "cantripUpgrade": { "type": "null" } } }
    }
  }
}
