{
  "$schema": "http://json-schema.org/draft-07/schema#",
  "additionalProperties": false,
  "$id": "https://github.com/Bobikobi/GAME/schema/json/kit.schema.json",
  "type": "object",
  "required": [
    "format",
    "id",
    "title",
    "creator",
    "type",
    "kit"
  ],
  "properties": {
    "$schema": {
      "type": "string"
    },
    "format": {
      "description": "Format version of this file.",
      "const": 1,
      "type": "number"
    },
    "id": {
      "pattern": "^[a-z0-9_-]+/[a-z0-9_-]+$",
      "maxLength": 80,
      "description": "Stable id \"creator/name\": lowercase letters, digits, \"_\" and \"-\". Never reused once merged.",
      "type": "string"
    },
    "title": {
      "minLength": 1,
      "maxLength": 40,
      "type": "string"
    },
    "creator": {
      "pattern": "^[a-z0-9_-]+$",
      "maxLength": 39,
      "description": "GitHub handle, lowercase.",
      "type": "string"
    },
    "description": {
      "minLength": 1,
      "maxLength": 240,
      "type": "string"
    },
    "ai_tools": {
      "maxItems": 8,
      "description": "AI tools used to make this file or its art. Required when any were used.",
      "type": "array",
      "items": {
        "minLength": 1,
        "maxLength": 60,
        "type": "string"
      }
    },
    "items": {
      "maxItems": 40,
      "type": "array",
      "items": {
        "additionalProperties": false,
        "type": "object",
        "required": [
          "id",
          "name",
          "kind",
          "rarity",
          "sprite",
          "weight"
        ],
        "properties": {
          "id": {
            "pattern": "^[a-z0-9_-]+/[a-z0-9_-]+$",
            "maxLength": 80,
            "description": "Stable id \"creator/name\": lowercase letters, digits, \"_\" and \"-\". Never reused once merged.",
            "type": "string"
          },
          "name": {
            "minLength": 1,
            "maxLength": 40,
            "type": "string"
          },
          "description": {
            "minLength": 1,
            "maxLength": 240,
            "type": "string"
          },
          "kind": {
            "anyOf": [
              {
                "const": "material",
                "type": "string"
              },
              {
                "const": "food",
                "type": "string"
              },
              {
                "const": "tool",
                "type": "string"
              },
              {
                "const": "fragment",
                "type": "string"
              },
              {
                "const": "treasure",
                "type": "string"
              }
            ]
          },
          "rarity": {
            "description": "Declared rarity. The check compares it with the rarity computed from the data.",
            "anyOf": [
              {
                "const": "common",
                "type": "string"
              },
              {
                "const": "uncommon",
                "type": "string"
              },
              {
                "const": "rare",
                "type": "string"
              },
              {
                "const": "legendary",
                "type": "string"
              }
            ]
          },
          "class": {
            "description": "A base class (plant, wood, fibre, ...) or the id of a class an extension declares under one of them.",
            "anyOf": [
              {
                "const": "plant",
                "type": "string"
              },
              {
                "const": "wood",
                "type": "string"
              },
              {
                "const": "fibre",
                "type": "string"
              },
              {
                "const": "stone",
                "type": "string"
              },
              {
                "const": "clay",
                "type": "string"
              },
              {
                "const": "mineral",
                "type": "string"
              },
              {
                "const": "metal",
                "type": "string"
              },
              {
                "const": "animal",
                "type": "string"
              },
              {
                "const": "meal",
                "type": "string"
              },
              {
                "const": "tool",
                "type": "string"
              },
              {
                "const": "garment",
                "type": "string"
              },
              {
                "const": "jewelry",
                "type": "string"
              },
              {
                "const": "relic",
                "type": "string"
              },
              {
                "pattern": "^[a-z0-9_-]+/[a-z0-9_-]+$",
                "maxLength": 80,
                "description": "Stable id \"creator/name\": lowercase letters, digits, \"_\" and \"-\". Never reused once merged.",
                "type": "string"
              }
            ]
          },
          "family": {
            "pattern": "^[a-z0-9_-]+$",
            "maxLength": 24,
            "description": "A group inside the class, for example \"berry\" for every berry. Open: any lowercase word.",
            "type": "string"
          },
          "tags": {
            "maxItems": 8,
            "uniqueItems": true,
            "description": "Open words that describe it, for example \"sweet\" or \"night\".",
            "type": "array",
            "items": {
              "pattern": "^[a-z0-9_-]+$",
              "maxLength": 24,
              "description": "A lowercase word.",
              "type": "string"
            }
          },
          "tier": {
            "minimum": 1,
            "maximum": 20,
            "description": "How advanced it is, from 1. The rarity says how often it appears; the tier says how far up the ladder it is. A tool has the tier of its \"tool\".",
            "type": "integer"
          },
          "sprite": {
            "pattern": "^[a-z0-9_-]+/[a-z0-9_-]+:[a-z0-9_/-]+$",
            "maxLength": 120,
            "description": "A frame of an art kit: \"<kit id>:<frame name>\", for example \"core/base-kit:items/berry\".",
            "type": "string"
          },
          "weight": {
            "minimum": 0,
            "maximum": 20,
            "description": "How heavy one unit is (DECISIONS.md, 2026-10-23): 0 for herbs, seeds and coins, 1 for most things, 3 for logs, stone and ore. The load against the weight limit sets the walking pace.",
            "type": "integer"
          },
          "food": {
            "additionalProperties": false,
            "type": "object",
            "required": [
              "satiety"
            ],
            "properties": {
              "satiety": {
                "minimum": 0,
                "maximum": 100,
                "description": "Health points restored when eaten (the meter holds 100 at the start). 0 for a food that only hurts.",
                "type": "integer"
              },
              "hurt": {
                "minimum": 1,
                "maximum": 100,
                "description": "Health points it takes when eaten, at any hour: a bad fruit or a raw herb (G30).",
                "type": "integer"
              }
            }
          },
          "tool": {
            "additionalProperties": false,
            "type": "object",
            "required": [
              "kind",
              "tier"
            ],
            "properties": {
              "kind": {
                "anyOf": [
                  {
                    "const": "basket",
                    "type": "string"
                  },
                  {
                    "const": "trowel",
                    "type": "string"
                  },
                  {
                    "const": "axe",
                    "type": "string"
                  },
                  {
                    "const": "staff",
                    "type": "string"
                  },
                  {
                    "const": "hammer",
                    "type": "string"
                  },
                  {
                    "const": "pick",
                    "type": "string"
                  },
                  {
                    "const": "rod",
                    "type": "string"
                  }
                ]
              },
              "tier": {
                "minimum": 1,
                "maximum": 5,
                "description": "Better tools work faster. The calculator clamps the total.",
                "type": "integer"
              },
              "uses": {
                "minimum": 1,
                "maximum": 1000,
                "description": "The tool wears out after this many uses. Left out, the tool lasts (DECISIONS.md, 2026-10-09: the crafting economy).",
                "type": "integer"
              }
            }
          },
          "fragment": {
            "additionalProperties": false,
            "description": "A found scrap that teaches a recipe.",
            "type": "object",
            "required": [
              "unlocks"
            ],
            "properties": {
              "unlocks": {
                "pattern": "^[a-z0-9_-]+/[a-z0-9_-]+$",
                "maxLength": 80,
                "description": "Stable id \"creator/name\": lowercase letters, digits, \"_\" and \"-\". Never reused once merged.",
                "type": "string"
              }
            }
          },
          "wear": {
            "additionalProperties": false,
            "description": "Something the character wears (DECISIONS.md, 2026-10-09: the upgrade). A worn item stays in the bag; it takes no place there, but its weight counts (2026-10-23).",
            "type": "object",
            "required": [
              "slot"
            ],
            "properties": {
              "slot": {
                "description": "Where it is worn: head, top, legs (trousers), outfit (takes the top and the legs), feet, jewelry (two places), hand (a tool) or back (one bag or one quiver).",
                "anyOf": [
                  {
                    "const": "head",
                    "type": "string"
                  },
                  {
                    "const": "top",
                    "type": "string"
                  },
                  {
                    "const": "legs",
                    "type": "string"
                  },
                  {
                    "const": "outfit",
                    "type": "string"
                  },
                  {
                    "const": "feet",
                    "type": "string"
                  },
                  {
                    "const": "jewelry",
                    "type": "string"
                  },
                  {
                    "const": "hand",
                    "type": "string"
                  },
                  {
                    "const": "back",
                    "type": "string"
                  }
                ]
              },
              "look": {
                "description": "A look: worn on the look layer over the work clothes. It shows and counts for nothing, so it has no effects and no set.",
                "type": "boolean"
              },
              "effects": {
                "minItems": 1,
                "maxItems": 2,
                "description": "Small bonuses while it is worn on the work layer. Two at most, each effect once.",
                "type": "array",
                "items": {
                  "additionalProperties": false,
                  "type": "object",
                  "required": [
                    "effect",
                    "value"
                  ],
                  "properties": {
                    "effect": {
                      "description": "What a worn item helps: gathering, digging or crafting speed, the craft chance, or rare finds.",
                      "anyOf": [
                        {
                          "const": "gather_speed_pct",
                          "type": "string"
                        },
                        {
                          "const": "dig_speed_pct",
                          "type": "string"
                        },
                        {
                          "const": "craft_speed_pct",
                          "type": "string"
                        },
                        {
                          "const": "craft_chance_pct",
                          "type": "string"
                        },
                        {
                          "const": "rare_find_pct",
                          "type": "string"
                        }
                      ]
                    },
                    "value": {
                      "minimum": 1,
                      "maximum": 100,
                      "description": "Percent, at most the cap of the item's tier (params.wear_effect_max_pct).",
                      "type": "integer"
                    }
                  }
                }
              },
              "set": {
                "pattern": "^[a-z0-9_-]+/[a-z0-9_-]+$",
                "maxLength": 80,
                "description": "A set of one creator: when every piece of it is worn, their effects grow by params.wear_set_bonus_pct.",
                "type": "string"
              },
              "places": {
                "minimum": 1,
                "maximum": 99,
                "description": "A bag on the back: the places it gives, instead of the hands' `bag_base`. Only a work piece on the back carries it (DECISIONS.md, 2026-10-23).",
                "type": "integer"
              },
              "load_bonus": {
                "minimum": 1,
                "maximum": 100,
                "description": "A bag on the back that moves the load to the hips: what it adds to the weight limit (`load.base` + might). Needs `places`.",
                "type": "integer"
              }
            }
          },
          "grade": {
            "additionalProperties": false,
            "description": "A finer grade of another item: the same good off a better animal. A stack splits by grade the way it splits by tier, so each grade is an item of its own; it keeps the plain item's class, kind, tier and value, and a recipe asks for it with \"min_grade\".",
            "type": "object",
            "required": [
              "of",
              "step"
            ],
            "properties": {
              "of": {
                "pattern": "^[a-z0-9_-]+/[a-z0-9_-]+$",
                "maxLength": 80,
                "description": "Stable id \"creator/name\": lowercase letters, digits, \"_\" and \"-\". Never reused once merged.",
                "type": "string"
              },
              "step": {
                "minimum": 2,
                "maximum": 3,
                "description": "2 is fine, 3 is true. A grade is read as a pose on the screen, never as a number (HUNTING section 5).",
                "type": "integer"
              }
            }
          },
          "premium_later": {
            "description": "Marks a look that may also be sold for premium currency once stage 6 brings it. Only a look item with no effect may carry it, and it stays obtainable in the game (DECISIONS.md, 2026-10-09).",
            "type": "boolean"
          }
        }
      }
    },
    "classes": {
      "maxItems": 10,
      "description": "Classes of your own, each under one base class.",
      "type": "array",
      "items": {
        "additionalProperties": false,
        "description": "A class of your own, declared under one base class. Every rule written for the base class holds for it.",
        "type": "object",
        "required": [
          "id",
          "name",
          "base"
        ],
        "properties": {
          "id": {
            "pattern": "^[a-z0-9_-]+/[a-z0-9_-]+$",
            "maxLength": 80,
            "description": "Stable id \"creator/name\": lowercase letters, digits, \"_\" and \"-\". Never reused once merged.",
            "type": "string"
          },
          "name": {
            "minLength": 1,
            "maxLength": 40,
            "type": "string"
          },
          "base": {
            "description": "plant (leaves, herbs, berries, fruit, roots, mushrooms), wood, fibre (reeds, grass, thread, cloth), stone, clay (clay, sand, pottery, brick), mineral (crystals, gems, salt, ore), metal (waits for its discovery), animal (feathers, wool, hide, bone), meal (cooked food), tool, garment (worn clothes), jewelry, relic (old things from the ruins).",
            "anyOf": [
              {
                "const": "plant",
                "type": "string"
              },
              {
                "const": "wood",
                "type": "string"
              },
              {
                "const": "fibre",
                "type": "string"
              },
              {
                "const": "stone",
                "type": "string"
              },
              {
                "const": "clay",
                "type": "string"
              },
              {
                "const": "mineral",
                "type": "string"
              },
              {
                "const": "metal",
                "type": "string"
              },
              {
                "const": "animal",
                "type": "string"
              },
              {
                "const": "meal",
                "type": "string"
              },
              {
                "const": "tool",
                "type": "string"
              },
              {
                "const": "garment",
                "type": "string"
              },
              {
                "const": "jewelry",
                "type": "string"
              },
              {
                "const": "relic",
                "type": "string"
              }
            ]
          },
          "description": {
            "minLength": 1,
            "maxLength": 240,
            "type": "string"
          }
        }
      }
    },
    "type": {
      "const": "kit",
      "type": "string"
    },
    "kit": {
      "additionalProperties": false,
      "type": "object",
      "required": [
        "sprites"
      ],
      "properties": {
        "palette": {
          "minItems": 2,
          "maxItems": 32,
          "description": "Only the core kit sets the palette. Every sprite of every kit must use these colors.",
          "type": "array",
          "items": {
            "pattern": "^#[0-9a-f]{6}$",
            "type": "string"
          }
        },
        "ramps": {
          "maxItems": 4,
          "description": "The ramps this kit draws with (ART_SYSTEM section 3). A sprite that names none uses the core palette only, so every kit written before this field stays valid.",
          "type": "array",
          "items": {
            "additionalProperties": false,
            "description": "A material or region ramp, declared by the kit that draws with it.",
            "type": "object",
            "required": [
              "id",
              "steps"
            ],
            "properties": {
              "id": {
                "pattern": "^[a-z0-9][a-z0-9-]*$",
                "maxLength": 32,
                "description": "Names the ramp so a sprite of this kit can cite it, for example \"meadow\" or \"wet-stone\".",
                "type": "string"
              },
              "for": {
                "anyOf": [
                  {
                    "const": "ground",
                    "type": "string"
                  },
                  {
                    "const": "vegetation",
                    "type": "string"
                  },
                  {
                    "const": "stone",
                    "type": "string"
                  },
                  {
                    "const": "wood",
                    "type": "string"
                  },
                  {
                    "const": "water",
                    "type": "string"
                  },
                  {
                    "const": "cloth",
                    "type": "string"
                  },
                  {
                    "const": "skin",
                    "type": "string"
                  },
                  {
                    "const": "light",
                    "type": "string"
                  }
                ]
              },
              "steps": {
                "minItems": 5,
                "maxItems": 6,
                "description": "Five or six steps from dark to light, the hue moving between them: a ramp that only changes value is not a ramp and the check says so.",
                "type": "array",
                "items": {
                  "pattern": "^#[0-9a-f]{6}$",
                  "description": "One step of the ramp, dark to light.",
                  "type": "string"
                }
              }
            }
          }
        },
        "sprites": {
          "description": "Sprite name (group/name) to its PNG. Groups: tiles, items, chars, props, ui.",
          "type": "object",
          "patternProperties": {
            "^[a-z0-9_-]+/[a-z0-9_/-]+$": {
              "additionalProperties": false,
              "type": "object",
              "required": [
                "file",
                "w",
                "h"
              ],
              "properties": {
                "file": {
                  "pattern": "^[a-z0-9_/-]+\\.png$",
                  "description": "PNG next to this file.",
                  "type": "string"
                },
                "w": {
                  "minimum": 8,
                  "maximum": 384,
                  "description": "Frame width in pixels. At density 1 (16 art pixels per tile) tiles/ and items/ are 16, chars/ are 16, a work pose 32 and props up to 64. At density 4 (64 art pixels per tile) tiles/ and items/ are 64, chars/ are 64, a work pose 128 and props up to 384.",
                  "type": "integer"
                },
                "h": {
                  "minimum": 8,
                  "maximum": 384,
                  "description": "Frame height in pixels. At density 1 tiles/ and items/ are 16, chars/ are 24, a work pose 32 and props up to 64. At density 4 tiles/ and items/ are 64, chars/ are 96, a work pose 128 and props up to 384.",
                  "type": "integer"
                },
                "frames": {
                  "minimum": 1,
                  "maximum": 16,
                  "description": "Animation frames laid side by side. Default 1.",
                  "type": "integer"
                },
                "ramps": {
                  "minItems": 1,
                  "maxItems": 4,
                  "description": "The ramps of this kit the sprite draws in, in order. The outline is the one-pixel ink line #1b1a2e on every sprite, whatever the ramps. A sprite that names none uses the core palette only.",
                  "type": "array",
                  "items": {
                    "pattern": "^[a-z0-9][a-z0-9-]*$",
                    "maxLength": 32,
                    "type": "string"
                  }
                },
                "density": {
                  "description": "Art pixels per tile side: 1 means 16 per tile (the 16x16 grid, the default), 4 means 64 per tile (the 64x64 grid, the standard for new art). The size rules of w and h follow it.",
                  "anyOf": [
                    {
                      "const": 1,
                      "type": "number"
                    },
                    {
                      "const": 4,
                      "type": "number"
                    }
                  ]
                },
                "stride": {
                  "additionalProperties": false,
                  "description": "A walk cycle that follows the distance walked (tools/anim). Its frames are the poses of the cycle in order and, last, the standing pose; `frames` is the poses plus one. Left out for a sheet whose frames are time-based.",
                  "type": "object",
                  "required": [
                    "travel_mpx",
                    "anchor"
                  ],
                  "properties": {
                    "travel_mpx": {
                      "minimum": 1000,
                      "maximum": 128000,
                      "description": "Thousandths of an art pixel (64 per tile) the body walks for one pose of the cycle: the pose shown is the nearest to art pixels walked / (travel_mpx / 1000), so a planted foot is where the ground is.",
                      "type": "integer"
                    },
                    "anchor": {
                      "description": "The point of the ground under the feet, in pixels of the frame: the sprite is drawn with it on the body's feet.",
                      "type": "array",
                      "items": [
                        {
                          "minimum": 0,
                          "maximum": 384,
                          "type": "integer"
                        },
                        {
                          "minimum": 0,
                          "maximum": 384,
                          "type": "integer"
                        }
                      ],
                      "additionalItems": false,
                      "minItems": 2,
                      "maxItems": 2
                    },
                    "bob": {
                      "description": "false when the sheet carries its own head travel and the feel layer's code bob is dropped for it (DECISIONS 2026-10-29: the bob is dropped for a clip that carries its own); left out, the bob plays on top of the poses.",
                      "type": "boolean"
                    }
                  }
                },
                "scale": {
                  "minimum": 0.1,
                  "maximum": 40,
                  "description": "The sprite's height in characters (NATURE section 1): a person is 1.0, a house 3.5. The check warns when a kind in NATURE section 1 or STYLE 1b falls under its floor. Left out when the sprite has no true size.",
                  "type": "number"
                },
                "source": {
                  "additionalProperties": false,
                  "description": "Where a generated sprite came from (ASSET_PATH section 4, item 2). Left out for a hand-drawn one.",
                  "type": "object",
                  "required": [
                    "spec",
                    "manifest"
                  ],
                  "properties": {
                    "spec": {
                      "minLength": 1,
                      "description": "The pnpm kit:gen spec file this sprite was made from, as a path from the repository root.",
                      "type": "string"
                    },
                    "manifest": {
                      "minLength": 1,
                      "description": "The manifest.jsonl line of that run, as <path>:<line number>.",
                      "type": "string"
                    }
                  }
                }
              }
            }
          }
        }
      }
    }
  }
}
