{
  "$schema": "http://json-schema.org/draft-07/schema#",
  "additionalProperties": false,
  "$id": "https://github.com/Bobikobi/GAME/schema/json/resource.schema.json",
  "type": "object",
  "required": [
    "format",
    "id",
    "title",
    "creator",
    "type",
    "node"
  ],
  "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": "resource",
      "type": "string"
    },
    "node": {
      "additionalProperties": false,
      "type": "object",
      "required": [
        "output",
        "yield",
        "active_ms",
        "respawn_ms",
        "count",
        "sprite",
        "level",
        "place"
      ],
      "properties": {
        "output": {
          "pattern": "^[a-z0-9_-]+/[a-z0-9_-]+$",
          "maxLength": 80,
          "description": "Stable id \"creator/name\": lowercase letters, digits, \"_\" and \"-\". Never reused once merged.",
          "type": "string"
        },
        "yield": {
          "minimum": 1,
          "maximum": 20,
          "description": "Units gathered per execution at perfect timing.",
          "type": "integer"
        },
        "active_ms": {
          "minimum": 0,
          "maximum": 3600000,
          "description": "Time the player is busy gathering once.",
          "type": "integer"
        },
        "respawn_ms": {
          "minimum": 0,
          "maximum": 3600000,
          "description": "Time before the same plant gives again. Waiting is never paid.",
          "type": "integer"
        },
        "count": {
          "minimum": 1,
          "maximum": 12,
          "description": "How many of these nodes the slot holds.",
          "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"
        },
        "sprite_empty": {
          "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"
        },
        "growth": {
          "minItems": 1,
          "maxItems": 3,
          "description": "Young stages of a node that grows back, from the youngest up (after `sprite_empty`, before `sprite`). The client shows the stage by how much of the respawn time has passed. Needs `sprite_empty`.",
          "type": "array",
          "items": {
            "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"
          }
        },
        "solid": {
          "description": "The node blocks the way, ready or not, as a tree does: a stump stays in the way until the tree is grown again. Left out: the player walks through it.",
          "type": "boolean"
        },
        "opens_after": {
          "minItems": 1,
          "maxItems": 4,
          "description": "Quests the node waits for, each with the line its refusal says. The node cannot be struck by a character who was offered one of them and has not finished it; a character who was offered none strikes the node as usual. Two entries may not both be offered to one wake pocket. The refusal costs nothing.",
          "type": "array",
          "items": {
            "additionalProperties": false,
            "type": "object",
            "required": [
              "quest"
            ],
            "properties": {
              "quest": {
                "pattern": "^[a-z0-9_-]+/[a-z0-9_-]+$",
                "maxLength": 80,
                "description": "Stable id \"creator/name\": lowercase letters, digits, \"_\" and \"-\". Never reused once merged.",
                "type": "string"
              },
              "line": {
                "minLength": 1,
                "maxLength": 80,
                "description": "The one short line the refusal says. Left out: \"It is not ready yet.\"",
                "type": "string"
              }
            }
          }
        },
        "tool": {
          "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"
            }
          ]
        },
        "work": {
          "description": "How the node is worked when it needs no tool. \"mine\": broken with blows that run by themselves after one press, as felling does; it trains digging, a pick or a hammer in the bag makes it faster (the best one, worn a little each time), and bare hands still do it. Left out: gathered by hand with the minigame of `game`.",
          "const": "mine",
          "type": "string"
        },
        "level": {
          "minimum": 1,
          "maximum": 20,
          "description": "Level required, in the profession the node trains: felling when the tool is an axe, fishing when it is a rod, digging when the node is mined, foraging otherwise.",
          "type": "integer"
        },
        "game": {
          "description": "The minigame played when gathering: \"timing\" (stop a marker in the middle, the default), \"strike\" (three timed hammer strikes, for stone), \"pick\" (press only for the ripe ones, for plants), \"snap\" (press fast, for wood) or \"fish\" (cast, wait for the bite and strike as the float goes under, then reel in; for a node fished with a rod).",
          "anyOf": [
            {
              "const": "timing",
              "type": "string"
            },
            {
              "const": "strike",
              "type": "string"
            },
            {
              "const": "pick",
              "type": "string"
            },
            {
              "const": "snap",
              "type": "string"
            },
            {
              "const": "fish",
              "type": "string"
            }
          ]
        },
        "hours": {
          "description": "Hours of the in-game day when it can be gathered, [from, to).",
          "type": "array",
          "items": [
            {
              "minimum": 0,
              "maximum": 23,
              "type": "integer"
            },
            {
              "minimum": 1,
              "maximum": 24,
              "type": "integer"
            }
          ],
          "additionalItems": false,
          "minItems": 2,
          "maxItems": 2
        },
        "place": {
          "additionalProperties": false,
          "description": "An empty resource slot of the map (`count` nodes inside it), and optionally single-node slots scattered over the map.",
          "type": "object",
          "required": [
            "slot"
          ],
          "properties": {
            "slot": {
              "pattern": "^[a-z0-9_-]+/[a-z0-9_-]+$",
              "maxLength": 80,
              "description": "Stable id \"creator/name\": lowercase letters, digits, \"_\" and \"-\". Never reused once merged.",
              "type": "string"
            },
            "scatter": {
              "minItems": 1,
              "uniqueItems": true,
              "description": "More resource slots that each hold exactly one of these nodes: a tree or a log standing where the map already shows one. They add supply only: `count`, the value and the opening's log floor follow the main slot.",
              "type": "array",
              "items": {
                "pattern": "^[a-z0-9_-]+/[a-z0-9_-]+$",
                "maxLength": 80,
                "description": "Stable id \"creator/name\": lowercase letters, digits, \"_\" and \"-\". Never reused once merged.",
                "type": "string"
              }
            },
            "water": {
              "description": "The nodes lie on the water tiles of the slot that touch the land, and are worked from the bank: fishing spots. Left out, they lie on the land tiles of the slot.",
              "const": true,
              "type": "boolean"
            }
          }
        }
      }
    }
  }
}
