{
  "$schema": "https://json-schema.org/draft/2020-12/schema",
  "$id": "https://fabledraft.com/schemas/world-1.json",
  "title": "FableDraft world import file, version 1",
  "description": "One world. Only the world 'name' and, per entity, 'type' and 'name' are required. Extra keys are allowed: the importer drops unknown keys with a warning. A reference ('ref') is an entity id from this file or an entity name (case-insensitive, first match). See docs/import-format.md.",
  "anyOf": [
    {"$ref": "#/$defs/world"},
    {
      "type": "object",
      "required": ["world"],
      "properties": {"world": {"$ref": "#/$defs/world"}}
    }
  ],
  "$defs": {
    "world": {
      "type": "object",
      "required": ["name"],
      "properties": {
        "$schema": {"type": "string"},
        "name": {"type": "string", "minLength": 1, "pattern": "\\S"},
        "description": {"type": "string", "description": "Markdown. Wikilinks allowed."},
        "style": {"type": "string", "maxLength": 12000, "description": "Art and writing style notes for the AI tools."},
        "portraitStyle": {"type": "string", "maxLength": 500, "description": "How character portraits look, e.g. 'cartoonish, bold outlines'. The first choice when making a portrait."},
        "created": {"type": "string", "description": "Timestamp, e.g. 2026-10-01T12:00:00Z."},
        "settings": {"$ref": "#/$defs/settings"},
        "calendar": {"$ref": "#/$defs/calendar"},
        "entities": {
          "description": "An object keyed by id, or a list. In a list, 'id' on each entity is optional.",
          "anyOf": [
            {"type": "object", "additionalProperties": {"$ref": "#/$defs/entity"}},
            {"type": "array", "items": {"$ref": "#/$defs/entity"}}
          ]
        },
        "links": {"type": "array", "items": {"$ref": "#/$defs/link"}},
        "journal": {"type": "array", "items": {"$ref": "#/$defs/journalEntry"}},
        "maps": {"type": "array", "items": {"$ref": "#/$defs/map"}},
        "nameCultures": {"type": "array", "items": {"$ref": "#/$defs/nameCulture"}},
        "nameLists": {"type": "array", "items": {"$ref": "#/$defs/nameList"}}
      }
    },

    "id": {"type": "string", "minLength": 1, "description": "Any short string. Kept when it is 8-12 letters, digits, _ or -; otherwise replaced, and every reference follows."},
    "ref": {"type": "string", "minLength": 1, "description": "An entity id from this file, or an entity name."},
    "refOrNull": {"anyOf": [{"$ref": "#/$defs/ref"}, {"type": "null"}, {"const": ""}]},
    "refs": {"type": "array", "items": {"$ref": "#/$defs/ref"}},
    "date": {
      "description": "In-world date: MM-DD-YYYY, MM-YYYY or YYYY. Year last, '-' or '/' between parts, negative years allowed.",
      "anyOf": [
        {"type": "string", "pattern": "^\\s*(?:(\\d{1,2})[-/](?:(\\d{1,2})[-/])?)?(-?\\d{1,6})\\s*$"},
        {"const": ""},
        {"type": "null"}
      ]
    },
    "text": {"type": ["string", "null"]},
    "timestamp": {"type": "string"},
    "stringList": {"type": "array", "items": {"type": "string"}},
    "nonEmptyStringList": {"type": "array", "minItems": 1, "items": {"type": "string", "pattern": "\\S"}},
    "imageDataUrl": {
      "type": "string",
      "pattern": "^data:image/(png|jpeg|jpg|gif|webp);base64,",
      "description": "A PNG, JPEG, GIF or WebP image as a base64 data URL. Large images are shrunk on import."
    },

    "entity": {
      "type": "object",
      "required": ["type", "name"],
      "properties": {
        "id": {"$ref": "#/$defs/id"},
        "type": {"enum": ["arc", "quest", "character", "location", "faction", "item", "event", "note"]},
        "name": {"type": "string", "minLength": 1, "pattern": "\\S"},
        "summary": {"type": "string", "description": "One or two lines. Markdown and wikilinks allowed."},
        "tagline": {"type": "string", "description": "Used as 'summary' when that is empty."},
        "body": {"type": "string", "description": "The main text. Markdown and wikilinks allowed."},
        "description": {"type": "string", "description": "Used as 'body' when that is empty (first non-empty of description, text, content)."},
        "text": {"type": "string", "description": "Used as 'body' when that is empty."},
        "content": {"type": "string", "description": "Used as 'body' when that is empty."},
        "status": {"type": "string", "description": "One of settings.statuses. An unknown status is added to the world's list. Default 'idea'."},
        "tags": {"$ref": "#/$defs/stringList"},
        "parentId": {"$ref": "#/$defs/refOrNull", "description": "arc under arc; quest under arc or quest; location under location. Other types have no parent."},
        "order": {"type": "number", "description": "Sort position among siblings."},
        "fields": {
          "type": "object",
          "description": "Type-specific fields (see the per-type rules below). Unknown keys are dropped with a warning.",
          "properties": {
            "parentRelation": {"$ref": "#/$defs/text", "description": "What the line to the parent says in the graph, e.g. 'part of'. Any type."}
          }
        },
        "portraitData": {"$ref": "#/$defs/imageDataUrl", "description": "Characters only."},
        "created": {"$ref": "#/$defs/timestamp"},
        "updated": {"$ref": "#/$defs/timestamp"}
      },
      "allOf": [
        {"if": {"properties": {"type": {"const": "arc"}}}, "then": {"properties": {"fields": {"$ref": "#/$defs/fields_arc"}}}},
        {"if": {"properties": {"type": {"const": "quest"}}}, "then": {"properties": {"fields": {"$ref": "#/$defs/fields_quest"}}}},
        {"if": {"properties": {"type": {"const": "character"}}}, "then": {"properties": {"fields": {"$ref": "#/$defs/fields_character"}}}},
        {"if": {"properties": {"type": {"const": "location"}}}, "then": {"properties": {"fields": {"$ref": "#/$defs/fields_location"}}}},
        {"if": {"properties": {"type": {"const": "faction"}}}, "then": {"properties": {"fields": {"$ref": "#/$defs/fields_faction"}}}},
        {"if": {"properties": {"type": {"const": "item"}}}, "then": {"properties": {"fields": {"$ref": "#/$defs/fields_item"}}}},
        {"if": {"properties": {"type": {"const": "event"}}}, "then": {"properties": {"fields": {"$ref": "#/$defs/fields_event"}}}}
      ]
    },

    "fields_arc": {
      "type": "object",
      "properties": {
        "startDate": {"$ref": "#/$defs/date"},
        "endDate": {"$ref": "#/$defs/date"},
        "kind": {"enum": ["main", "sub", null]},
        "theme": {"$ref": "#/$defs/text"},
        "hook": {"$ref": "#/$defs/text"},
        "climax": {"$ref": "#/$defs/text"},
        "resolution": {"$ref": "#/$defs/text"}
      }
    },
    "fields_quest": {
      "type": "object",
      "properties": {
        "giverId": {"$ref": "#/$defs/refOrNull", "description": "a character, or an item such as a letter the player reads"},
        "turnInId": {"$ref": "#/$defs/refOrNull", "description": "a character"},
        "difficulty": {"enum": ["easy", "normal", "hard", "deadly", null]},
        "timeLimit": {"$ref": "#/$defs/text"},
        "optional": {"type": "boolean"},
        "locationIds": {"$ref": "#/$defs/refs", "description": "locations"},
        "itemIds": {"$ref": "#/$defs/refs", "description": "items"},
        "prerequisiteIds": {"$ref": "#/$defs/refs", "description": "quests"},
        "hook": {"$ref": "#/$defs/text"},
        "journalText": {"type": ["string", "null"], "maxLength": 2000, "description": "What the player reads in the journal. A longer text is cut, with a warning."},
        "objectives": {"type": "array", "items": {"$ref": "#/$defs/questStep"}},
        "ending": {"$ref": "#/$defs/text"},
        "failure": {"$ref": "#/$defs/text"},
        "rewards": {"type": "array", "items": {"$ref": "#/$defs/questReward"}}
      }
    },
    "questStep": {
      "type": "object",
      "properties": {
        "id": {"type": "string"},
        "text": {"type": "string"},
        "done": {"type": "boolean"},
        "locationId": {"$ref": "#/$defs/refOrNull", "description": "a location"},
        "itemIds": {"$ref": "#/$defs/refs", "description": "items"},
        "characterIds": {"$ref": "#/$defs/refs", "description": "characters"},
        "talkId": {"type": ["string", "null"], "description": "The id of a talk; reaching its end finishes the step. Not an entity reference."},
        "action": {"enum": ["read", "collect", "kill", "talk", "go", "use", "other", null], "description": "What the step asks of the player. Missing means a story-only step. Another word is kept as written, with a warning."},
        "count": {"type": ["integer", "null"], "minimum": 1, "maximum": 9999, "description": "How many times, for collect and kill. Missing means 1."},
        "targetText": {"type": ["string", "null"], "maxLength": 120, "description": "For collect and kill: a target with no entry, like pig."},
        "journalText": {"type": ["string", "null"], "maxLength": 2000, "description": "What the game logs when the step finishes."}
      }
    },
    "questReward": {
      "type": "object",
      "properties": {
        "id": {"type": "string"},
        "kind": {"enum": ["item", "xp", "gold", "reputation", "other"]},
        "itemId": {"$ref": "#/$defs/refOrNull", "description": "an item, when kind is 'item'"},
        "amount": {"type": ["number", "null"]},
        "text": {"type": "string"}
      }
    },
    "fields_character": {
      "type": "object",
      "properties": {
        "born": {"$ref": "#/$defs/date"},
        "died": {"$ref": "#/$defs/date"},
        "role": {"enum": ["pc", "npc", "villain", "ally", "neutral", null]},
        "race": {"$ref": "#/$defs/text"},
        "class": {"$ref": "#/$defs/text"},
        "age": {"$ref": "#/$defs/text"},
        "pronouns": {"$ref": "#/$defs/text"},
        "homeLocationId": {"$ref": "#/$defs/refOrNull", "description": "a location"},
        "factionIds": {"$ref": "#/$defs/refs", "description": "factions"},
        "appearance": {"$ref": "#/$defs/text"},
        "personality": {"$ref": "#/$defs/text"},
        "motivation": {"$ref": "#/$defs/text"},
        "secrets": {"$ref": "#/$defs/text"},
        "statblock": {"$ref": "#/$defs/text"},
        "portraitUrl": {"$ref": "#/$defs/text"},
        "talks": {"type": "array", "maxItems": 50, "items": {"$ref": "#/$defs/talk"}, "description": "The first talk whose conditions pass plays."}
      }
    },
    "talk": {
      "type": "object",
      "properties": {
        "id": {"type": "string", "pattern": "^[A-Za-z0-9_-]{1,64}$", "description": "Unique in the world and not an entity id, e.g. t-k3j9x2."},
        "title": {"type": "string", "maxLength": 120},
        "conditions": {"$ref": "#/$defs/talkConditions"},
        "lines": {"type": "array", "maxItems": 200, "items": {"$ref": "#/$defs/talkLine"}}
      }
    },
    "talkLine": {
      "type": "object",
      "properties": {
        "id": {"type": "string", "pattern": "^[A-Za-z0-9_-]{1,64}$", "description": "Unique in its talk."},
        "speaker": {"enum": ["character", "player"]},
        "text": {"type": "string", "maxLength": 2000},
        "conditions": {"$ref": "#/$defs/talkConditions"},
        "effects": {"$ref": "#/$defs/talkEffects"},
        "next": {"type": ["string", "null"], "description": "null = the line below, 'end', or a line id of this talk. Ignored when the line has choices."},
        "choices": {"type": "array", "maxItems": 8, "items": {"$ref": "#/$defs/talkChoice"}}
      }
    },
    "talkChoice": {
      "type": "object",
      "properties": {
        "id": {"type": "string", "pattern": "^[A-Za-z0-9_-]{1,64}$"},
        "text": {"type": "string", "maxLength": 2000},
        "conditions": {"$ref": "#/$defs/talkConditions"},
        "effects": {"$ref": "#/$defs/talkEffects"},
        "next": {"type": ["string", "null"], "description": "A line id of this talk, 'end', or null for the line below."}
      }
    },
    "talkConditions": {
      "type": "array",
      "maxItems": 10,
      "items": {
        "type": "object",
        "required": ["kind"],
        "properties": {
          "kind": {"enum": ["quest", "step", "item", "switch"]},
          "questId": {"$ref": "#/$defs/ref", "description": "a quest"},
          "stepId": {"type": "string", "description": "a step id of that quest"},
          "state": {"enum": ["not_started", "active", "done", "failed", "not_done"]},
          "itemId": {"$ref": "#/$defs/ref", "description": "an item"},
          "has": {"type": "boolean"},
          "switchId": {"type": "string", "description": "a settings.switches id"},
          "on": {"type": "boolean"}
        }
      }
    },
    "talkEffects": {
      "type": "array",
      "maxItems": 10,
      "items": {
        "type": "object",
        "required": ["kind"],
        "properties": {
          "kind": {"enum": ["start_quest", "finish_step", "finish_quest", "set_switch", "give_item", "take_item"]},
          "questId": {"$ref": "#/$defs/ref", "description": "a quest"},
          "stepId": {"type": "string", "description": "a step id of that quest"},
          "itemId": {"$ref": "#/$defs/ref", "description": "an item"},
          "switchId": {"type": "string", "description": "a settings.switches id"},
          "on": {"type": "boolean"}
        }
      }
    },
    "fields_location": {
      "type": "object",
      "properties": {
        "founded": {"$ref": "#/$defs/date"},
        "kind": {"enum": ["continent", "region", "city", "town", "dungeon", "building", "wilderness", "other", null]},
        "population": {"$ref": "#/$defs/text"},
        "government": {"$ref": "#/$defs/text"}
      }
    },
    "fields_faction": {
      "type": "object",
      "properties": {
        "founded": {"$ref": "#/$defs/date"},
        "dissolved": {"$ref": "#/$defs/date"},
        "leaderId": {"$ref": "#/$defs/refOrNull", "description": "a character"},
        "baseLocationId": {"$ref": "#/$defs/refOrNull", "description": "a location"},
        "alignment": {"$ref": "#/$defs/text"},
        "goals": {"$ref": "#/$defs/text"}
      }
    },
    "fields_item": {
      "type": "object",
      "properties": {
        "created": {"$ref": "#/$defs/date"},
        "category": {"$ref": "#/$defs/text", "description": "One of settings.itemCategories."},
        "kind": {"enum": ["weapon", "armor", "artifact", "consumable", "key", "treasure", "other", null]},
        "rarity": {"enum": ["common", "uncommon", "rare", "very_rare", "legendary", null]},
        "value": {"type": ["number", "null"]},
        "ownerId": {"$ref": "#/$defs/refOrNull", "description": "a character or a faction"}
      }
    },
    "fields_event": {
      "type": "object",
      "properties": {
        "date": {"$ref": "#/$defs/date"},
        "endDate": {"$ref": "#/$defs/date"},
        "when": {"$ref": "#/$defs/text"},
        "arcId": {"$ref": "#/$defs/refOrNull", "description": "an arc"},
        "locationId": {"$ref": "#/$defs/refOrNull", "description": "a location"}
      }
    },

    "link": {
      "type": "object",
      "required": ["fromId", "toId"],
      "properties": {
        "id": {"$ref": "#/$defs/id"},
        "fromId": {"$ref": "#/$defs/ref"},
        "toId": {"$ref": "#/$defs/ref"},
        "type": {"type": "string", "description": "Usually one of settings.relationTypes. Default 'other'."},
        "label": {"type": "string"},
        "notes": {"type": "string"},
        "created": {"$ref": "#/$defs/timestamp"}
      }
    },
    "journalEntry": {
      "type": "object",
      "properties": {
        "id": {"$ref": "#/$defs/id"},
        "date": {"type": "string", "description": "Free text, usually the real session date."},
        "title": {"type": "string"},
        "body": {"type": "string"},
        "entityIds": {"$ref": "#/$defs/refs"},
        "created": {"$ref": "#/$defs/timestamp"},
        "updated": {"$ref": "#/$defs/timestamp"}
      }
    },
    "map": {
      "type": "object",
      "properties": {
        "id": {"$ref": "#/$defs/id"},
        "name": {"type": "string"},
        "locationId": {"$ref": "#/$defs/refOrNull", "description": "a location"},
        "imageData": {"$ref": "#/$defs/imageDataUrl"},
        "markers": {"type": "array", "items": {"$ref": "#/$defs/marker"}},
        "created": {"$ref": "#/$defs/timestamp"},
        "updated": {"$ref": "#/$defs/timestamp"}
      }
    },
    "marker": {
      "type": "object",
      "required": ["entityId", "x", "y"],
      "properties": {
        "id": {"$ref": "#/$defs/id"},
        "entityId": {"$ref": "#/$defs/ref"},
        "x": {"type": "number", "minimum": 0, "maximum": 1, "description": "0 = left edge, 1 = right edge."},
        "y": {"type": "number", "minimum": 0, "maximum": 1, "description": "0 = top edge, 1 = bottom edge."},
        "underId": {"$ref": "#/$defs/refOrNull", "description": "a location this marker sits inside"}
      }
    },

    "settings": {
      "type": "object",
      "properties": {
        "relationTypes": {"$ref": "#/$defs/nonEmptyStringList"},
        "statuses": {"$ref": "#/$defs/nonEmptyStringList"},
        "itemCategories": {"type": "array", "items": {"type": "string", "pattern": "\\S"}},
        "switches": {
          "type": "array",
          "maxItems": 200,
          "items": {
            "type": "object",
            "required": ["name"],
            "properties": {
              "id": {"type": "string", "pattern": "^[A-Za-z0-9_-]{1,64}$", "description": "e.g. sw-a1b2c3; kept as written"},
              "name": {"type": "string", "pattern": "\\S", "maxLength": 120},
              "on": {"type": "boolean", "description": "The starting value."}
            }
          }
        }
      }
    },
    "calendar": {
      "type": "object",
      "properties": {
        "dateOrder": {"const": "mdy"},
        "yearLabel": {"type": "string"},
        "monthNames": {"type": "array", "maxItems": 12, "items": {"type": "string"}},
        "daysPerMonth": {"type": ["integer", "null"], "minimum": 1, "maximum": 31},
        "weekDays": {"type": "array", "maxItems": 14, "items": {"type": "string"}},
        "eras": {"type": "array", "items": {"$ref": "#/$defs/era"}}
      }
    },
    "era": {
      "type": "object",
      "required": ["name", "start"],
      "properties": {
        "id": {"type": "string", "pattern": "^[A-Za-z0-9_-]{1,64}$"},
        "name": {"type": "string", "minLength": 1, "pattern": "\\S"},
        "start": {"type": "integer"},
        "end": {"type": ["integer", "null"]},
        "color": {"type": "string", "pattern": "^(#[0-9a-fA-F]{6})?$"},
        "summary": {"type": "string"}
      }
    },

    "nameCulture": {
      "type": "object",
      "properties": {
        "id": {"$ref": "#/$defs/id"},
        "name": {"type": "string", "maxLength": 80},
        "style": {"type": "string", "maxLength": 1000},
        "order": {"type": "number"}
      }
    },
    "nameList": {
      "type": "object",
      "properties": {
        "id": {"$ref": "#/$defs/id"},
        "title": {"type": "string", "maxLength": 120},
        "kind": {"enum": ["character", "place", "region", "town", "faction", "item", "other"]},
        "culture": {"type": "string", "description": "The id of a nameCultures entry in this file."},
        "structure": {"enum": ["single", "given_family"]},
        "style": {"type": "string", "maxLength": 1000},
        "names": {
          "type": "array",
          "maxItems": 500,
          "items": {
            "anyOf": [
              {"type": "string"},
              {"type": "object", "required": ["name"], "properties": {"name": {"type": "string"}, "note": {"type": "string"}, "starred": {"type": "boolean"}}}
            ]
          }
        },
        "parts": {
          "type": "object",
          "properties": {
            "prefixes": {"$ref": "#/$defs/stringList"},
            "suffixes": {"$ref": "#/$defs/stringList"}
          }
        },
        "isPublic": {"type": "boolean"},
        "order": {"type": "number"}
      }
    }
  }
}
