@pixelvibing/cli 0.34.0 → 0.36.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -610,6 +610,8 @@ function createWebApi(fetcher = (input, init2) => fetch(input, init2), base = AP
610
610
  /** Start a generation for a project, answering its job's id, or `TooFewCredits` (R3.2, R3.4). */
611
611
  generate: (kind, body) => paying2(async () => (await json2(`/generations/${id(kind)}`, { method: "POST", body })).id),
612
612
  job: (jobId) => json2(`/jobs/${id(jobId)}`),
613
+ /** Ask a job again with its own input, answering the new job's id, or `TooFewCredits` (specs/generation-jobs/ R6). */
614
+ redo: (jobId) => paying2(async () => (await json2(`/jobs/${id(jobId)}/redo`, { method: "POST" })).id),
613
615
  /** Cancel a job no worker has taken, answering it and the credits that came back; 409 once one has (specs/generation-jobs/ R5). */
614
616
  cancelJob: (jobId) => json2(`/jobs/${id(jobId)}/cancel`, { method: "POST" }),
615
617
  jobs: (projectId, before = null, kinds = null) => json2(
@@ -1041,7 +1043,6 @@ function nameProblem(name) {
1041
1043
  }
1042
1044
  return null;
1043
1045
  }
1044
- const HUD_PIECE_NAMES = ["health-bar", "mana-bar", "button", "inventory-slot", "panel-frame", "minimap-frame"];
1045
1046
  const ICON_SIZES = [16, 32, 64];
1046
1047
  const DEFAULT_ICON_SIZE = 32;
1047
1048
  const MAX_ICONS = 16;
@@ -3923,27 +3924,16 @@ const GENERATED = {
3923
3924
  object: { references: 8, words: "an object" },
3924
3925
  creature: { references: 3, words: "a creature" },
3925
3926
  "level-design": { references: 3, words: "a level design" },
3926
- hud: { references: 0, words: "a HUD set" },
3927
3927
  icons: { references: 0, words: "an icon set" },
3928
- "hud-mockup": { references: 3, words: "a HUD mockup" },
3929
- "ui-sketch": { references: 3, words: "a UI sketch" }
3928
+ "ui-sketch": { references: 3, words: "a UI" }
3930
3929
  };
3930
+ const KIND_NAMES = { ui: "ui-sketch" };
3931
3931
  function listOf(given, what) {
3932
3932
  const items = given.split(",").map((item) => item.trim());
3933
3933
  if (items.some((item) => item === "")) throw new CliError(`${what} is a comma-separated list with no empty entry`);
3934
3934
  return items;
3935
3935
  }
3936
- function interfaceFields(kind, args) {
3937
- if (kind === "hud") {
3938
- if (stringOf(args, "names") !== void 0 || stringOf(args, "purpose") !== void 0) throw new CliError("--names and --purpose are an icon set's");
3939
- if (stringOf(args, "size") !== void 0) throw new CliError("--size is an object's or an icon set's");
3940
- const pieces = listOf(stringOf(args, "pieces") ?? HUD_PIECE_NAMES.join(","), "--pieces");
3941
- const unknown = pieces.find((piece) => !HUD_PIECE_NAMES.includes(piece));
3942
- if (unknown !== void 0) throw new CliError(`${unknown} is not a HUD piece: ${HUD_PIECE_NAMES.join(", ")}`);
3943
- if (new Set(pieces).size !== pieces.length) throw new CliError("--pieces names a piece twice");
3944
- return { fields: { pieces }, words: `a HUD set of ${pieces.join(", ")}` };
3945
- }
3946
- if (stringOf(args, "pieces") !== void 0) throw new CliError("--pieces is a HUD set's");
3936
+ function iconFields(args) {
3947
3937
  const given = stringOf(args, "names");
3948
3938
  if (given === void 0) throw new CliError('name the icons with --names, such as --names "red potion,key,coin"');
3949
3939
  const names = listOf(given, "--names");
@@ -3961,7 +3951,7 @@ function interfaceFields(kind, args) {
3961
3951
  }
3962
3952
  const MAX_SKETCH_BYTES = 256 * 1024;
3963
3953
  async function sketchOf(context, path, instruction) {
3964
- if (path === void 0) throw new CliError("lay the sketch out in a JSON file and name it with --sketch, such as --sketch hud.json");
3954
+ if (path === void 0) throw new CliError("lay the UI out in a JSON file and name it with --sketch, such as --sketch ui.json");
3965
3955
  const file = resolve(context.io.cwd, path);
3966
3956
  let text2;
3967
3957
  try {
@@ -3998,7 +3988,7 @@ async function sketchOf(context, path, instruction) {
3998
3988
  const labels = sketch.shapes.map((shape) => shape.label.trim());
3999
3989
  return {
4000
3990
  sketch,
4001
- words: `a UI sketch of ${labels.length} shape${labels.length === 1 ? "" : "s"} (${labels.join(", ")}) on ${canvas.width} × ${canvas.height}`
3991
+ words: `a UI of ${labels.length} shape${labels.length === 1 ? "" : "s"} (${labels.join(", ")}) on ${canvas.width} × ${canvas.height}`
4002
3992
  };
4003
3993
  }
4004
3994
  const MAX_DESIGN_TILES = 64;
@@ -4021,11 +4011,12 @@ async function referenceOf(context, path) {
4021
4011
  if (!isPng(bytes)) throw new CliError(`the reference ${path} is not a PNG`);
4022
4012
  return bytes;
4023
4013
  }
4024
- async function requestOf(context, kind, args) {
4014
+ async function requestOf(context, asked, args) {
4015
+ const kind = KIND_NAMES[asked] ?? asked;
4025
4016
  const known = GENERATED[kind];
4026
- if (known === void 0) throw new CliError(`generate makes ${Object.keys(GENERATED).join(", ")}, not ${kind}`);
4017
+ if (known === void 0) throw new CliError(`generate makes ${Object.keys(GENERATED).join(", ")} (or ui), not ${asked}`);
4027
4018
  const instruction = stringOf(args, "instruction")?.trim() ?? "";
4028
- const set = kind === "hud" || kind === "icons";
4019
+ const set = kind === "icons";
4029
4020
  if (instruction === "" && !set) throw new CliError("say what to make with --instruction");
4030
4021
  if (stringOf(args, "quality") !== void 0)
4031
4022
  context.io.warn("--quality is the platform's to choose now: it draws each asset at the quality it needs, and nothing was sent for it.\n");
@@ -4033,8 +4024,8 @@ async function requestOf(context, kind, args) {
4033
4024
  if (paths.length > known.references)
4034
4025
  throw new CliError(known.references === 0 ? `${known.words} takes no reference` : `${known.words} takes at most ${known.references} references`);
4035
4026
  if (kind === "ui-sketch") {
4036
- for (const flag of ["pieces", "names", "purpose", "size", "tiles"])
4037
- if (stringOf(args, flag) !== void 0) throw new CliError(`--${flag} is not a UI sketch's: its shapes are in the --sketch file`);
4027
+ for (const flag of ["names", "purpose", "size", "tiles"])
4028
+ if (stringOf(args, flag) !== void 0) throw new CliError(`--${flag} is not a UI's: its shapes are in the --sketch file`);
4038
4029
  const { sketch, words: words2 } = await sketchOf(context, stringOf(args, "sketch"), instruction);
4039
4030
  const styles = [];
4040
4031
  for (const path of paths) styles.push(await referenceOf(context, path));
@@ -4045,15 +4036,14 @@ async function requestOf(context, kind, args) {
4045
4036
  words: `${words2}${styles.length > 0 ? `, in the style of ${styles.length} reference${styles.length === 1 ? "" : "s"}` : ""}: ${instruction}`
4046
4037
  };
4047
4038
  }
4048
- if (stringOf(args, "sketch") !== void 0) throw new CliError("--sketch is a UI sketch's");
4039
+ if (stringOf(args, "sketch") !== void 0) throw new CliError("--sketch is a UI's");
4049
4040
  if (set) {
4050
- const { fields, words: words2 } = interfaceFields(kind, args);
4041
+ const { fields, words: words2 } = iconFields(args);
4051
4042
  if (stringOf(args, "tiles") !== void 0) throw new CliError("--tiles is a level design's");
4052
4043
  const body2 = { ...fields, ...instruction === "" ? {} : { instruction } };
4053
4044
  return { kind, body: body2, words: instruction === "" ? words2 : `${words2}; ${instruction}` };
4054
4045
  }
4055
- for (const flag of ["pieces", "names", "purpose"])
4056
- if (stringOf(args, flag) !== void 0) throw new CliError(`--${flag} is ${flag === "pieces" ? "a HUD set's" : "an icon set's"}`);
4046
+ for (const flag of ["names", "purpose"]) if (stringOf(args, flag) !== void 0) throw new CliError(`--${flag} is an icon set's`);
4057
4047
  const references = [];
4058
4048
  for (const path of paths) references.push(await referenceOf(context, path));
4059
4049
  const body = { instruction };
@@ -4100,27 +4090,26 @@ async function followed(context, platform, jobId) {
4100
4090
  }
4101
4091
  const generate = {
4102
4092
  name: "generate",
4103
- summary: "Generate an object, a creature, a level design, a HUD set, icons, a HUD mockup or a UI sketch on the platform, then pull it",
4104
- usage: "<object|creature|level-design|hud|icons|hud-mockup|ui-sketch> [--instruction <text>] [--size <c>x<r>|<px>] [--tiles <c>x<r>] [--pieces <list>] [--names <list>] [--purpose skill|item] [--sketch <json>] [--reference <png>]… [--yes]",
4093
+ summary: "Generate an object, a creature, a level design, icons or a UI on the platform, then pull it",
4094
+ usage: "<object|creature|level-design|icons|ui> [--instruction <text>] [--size <c>x<r>|<px>] [--tiles <c>x<r>] [--names <list>] [--purpose skill|item] [--sketch <json>] [--reference <png>]… [--yes]",
4105
4095
  options: {
4106
4096
  instruction: {
4107
4097
  type: "string",
4108
4098
  value: "<text>",
4109
- description: "what to make, in your own words; a HUD set's or icon set's theme; a UI sketch's set, in a line"
4099
+ description: "what to make, in your own words; an icon set's theme; a UI's set, in a line"
4110
4100
  },
4111
4101
  size: {
4112
4102
  type: "string",
4113
4103
  value: "<cols>x<rows>|<px>",
4114
4104
  description: "an object's footprint in tiles, 1x1 unless given; an icon's size, 16, 32 or 64 px, 32 unless given"
4115
4105
  },
4116
- pieces: { type: "string", value: "<list>", description: `a HUD set's pieces, comma-separated: ${HUD_PIECE_NAMES.join(", ")}; all unless given` },
4117
4106
  names: { type: "string", value: "<list>", description: `an icon set's icons, comma-separated, at most ${MAX_ICONS}` },
4118
4107
  purpose: { type: "string", value: "skill|item", description: "whether an icon set is skills on badges or items alone; item unless given" },
4119
4108
  tiles: { type: "string", value: "<cols>x<rows>", description: "how many tiles a level design is across, 6x4 unless given" },
4120
4109
  sketch: {
4121
4110
  type: "string",
4122
4111
  value: "<json>",
4123
- description: "a UI sketch's file: { canvas: { width, height }, shapes: [{ label, kind, x, y, width, height, radius?, sides?, caption?, note?, border?, hollow? }] }"
4112
+ description: "a UI's layout file, as the Skill's references/ui.schema.json says: { canvas: { width, height }, shapes: [{ label, kind, x, y, width, height, radius?, sides?, caption?, note?, border?, hollow? }] }"
4124
4113
  },
4125
4114
  reference: { type: "string", multiple: true, value: "<png>", description: "an image to draw from" },
4126
4115
  quality: { type: "string", value: "<quality>", description: "the platform's to choose now; ignored, and said so" },
@@ -4601,7 +4590,7 @@ const move = {
4601
4590
  );
4602
4591
  }
4603
4592
  };
4604
- const version = "0.34.0";
4593
+ const version = "0.36.0";
4605
4594
  const manifest = {
4606
4595
  version
4607
4596
  };
@@ -4681,13 +4670,31 @@ const HARNESS_FILES = { claude: ".mcp.json", codex: ".codex/config.toml" };
4681
4670
  const HARNESSES = { claude: ".claude", codex: ".codex" };
4682
4671
  const SKILLS_MANIFEST = "pixelvibing-skills.json";
4683
4672
  const sha256 = (text2) => createHash("sha256").update(text2).digest("hex");
4673
+ function skillHash(text2, files = []) {
4674
+ if (files.length === 0) return sha256(text2);
4675
+ const hash = createHash("sha256").update(text2);
4676
+ for (const file of files) hash.update(`\0${file.path}\0`).update(file.text);
4677
+ return hash.digest("hex");
4678
+ }
4679
+ async function referencesOf(directory) {
4680
+ let names;
4681
+ try {
4682
+ names = (await readdir(join(directory, "references"), { withFileTypes: true })).filter((entry) => entry.isFile()).map((entry) => entry.name);
4683
+ } catch {
4684
+ return [];
4685
+ }
4686
+ const files = [];
4687
+ for (const name of names.sort()) files.push({ path: `references/${name}`, text: await readFile(join(directory, "references", name), "utf8") });
4688
+ return files;
4689
+ }
4684
4690
  const SHIPPED_DIRECTORY = fileURLToPath(new URL("../skills/", import.meta.url));
4685
4691
  async function shippedSkills(directory = SHIPPED_DIRECTORY) {
4686
4692
  const names = (await readdir(directory, { withFileTypes: true })).filter((entry) => entry.isDirectory()).map((entry) => entry.name);
4687
4693
  const skills = [];
4688
4694
  for (const name of names.sort()) {
4689
4695
  const text2 = await readFile(join(directory, name, "SKILL.md"), "utf8");
4690
- skills.push({ name, text: text2, sha256: sha256(text2) });
4696
+ const files = await referencesOf(join(directory, name));
4697
+ skills.push({ name, text: text2, ...files.length > 0 ? { files } : {}, sha256: skillHash(text2, files) });
4691
4698
  }
4692
4699
  return skills;
4693
4700
  }
@@ -4731,6 +4738,11 @@ async function writeSkill(root, harness, skill) {
4731
4738
  const path = skillPath(root, harness, skill.name);
4732
4739
  await mkdir(join(path, ".."), { recursive: true });
4733
4740
  await writeFile(path, skill.text);
4741
+ for (const file of skill.files ?? []) {
4742
+ const at = join(path, "..", file.path);
4743
+ await mkdir(join(at, ".."), { recursive: true });
4744
+ await writeFile(at, file.text);
4745
+ }
4734
4746
  }
4735
4747
  async function textAt(path) {
4736
4748
  try {
@@ -4739,17 +4751,25 @@ async function textAt(path) {
4739
4751
  return null;
4740
4752
  }
4741
4753
  }
4754
+ async function hashThere(root, harness, skill) {
4755
+ const path = skillPath(root, harness, skill.name);
4756
+ const text2 = await textAt(path);
4757
+ if (text2 === null) return null;
4758
+ const files = [];
4759
+ for (const file of skill.files ?? []) files.push({ path: file.path, text: await textAt(join(path, "..", file.path)) ?? "" });
4760
+ return skillHash(text2, files);
4761
+ }
4742
4762
  async function installMissing(root, harness, skills) {
4743
4763
  const manifest2 = await readManifest(root, harness) ?? {};
4744
4764
  const installed = [];
4745
4765
  const left = [];
4746
4766
  for (const skill of skills) {
4747
- const there = await textAt(skillPath(root, harness, skill.name));
4767
+ const there = await hashThere(root, harness, skill);
4748
4768
  if (there === null) {
4749
4769
  await writeSkill(root, harness, skill);
4750
4770
  manifest2[skill.name] = skill.sha256;
4751
4771
  installed.push(skill.name);
4752
- } else if (sha256(there) === skill.sha256) manifest2[skill.name] = skill.sha256;
4772
+ } else if (there === skill.sha256) manifest2[skill.name] = skill.sha256;
4753
4773
  else if (manifest2[skill.name] === void 0) left.push(skill.name);
4754
4774
  }
4755
4775
  await writeManifest(root, harness, manifest2);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pixelvibing/cli",
3
- "version": "0.34.0",
3
+ "version": "0.36.0",
4
4
  "description": "PixelVibing: pixel art game assets from the command line, and the Skills that teach coding assistants to make them.",
5
5
  "license": "MIT",
6
6
  "homepage": "https://pixelvibing.com",
@@ -1,6 +1,6 @@
1
1
  ---
2
2
  name: pixelvibing
3
- description: Make pixel art for this game with pixel-vibing-cli — objects, creatures, level designs and interface art (a HUD, skill and item icons, pieces laid out in a sketch) generated on the PixelVibing platform, listed, pulled into the project's art and its folder, and exported or sealed for the game. Use when the game needs a sprite, a prop, a creature, a level's art, its HUD or its icons, or art already made for it.
3
+ description: Make pixel art for this game with pixel-vibing-cli — objects, creatures, level designs and interface art (a UI laid out as JSON, skill and item icons) generated on the PixelVibing platform, listed, pulled into the project's art and its folder, and exported or sealed for the game. Use when the game needs a sprite, a prop, a creature, a level's art, its HUD or its icons, or art already made for it.
4
4
  ---
5
5
 
6
6
  # PixelVibing art for this game
@@ -32,10 +32,8 @@ Progress and errors go to standard error, and a failure exits 1.
32
32
  pixel-vibing-cli generate object --instruction "<what>" --size <cols>x<rows> --yes --json
33
33
  pixel-vibing-cli generate creature --instruction "<what>" [--reference <png>]... --yes --json
34
34
  pixel-vibing-cli generate level-design --instruction "<what>" --tiles <cols>x<rows> --yes --json
35
- pixel-vibing-cli generate hud [--pieces <list>] [--instruction "<theme>"] --yes --json
36
35
  pixel-vibing-cli generate icons --names "<name>,<name>,..." --purpose <skill|item> [--size <16|32|64>] --yes --json
37
- pixel-vibing-cli generate hud-mockup --instruction "<the HUD>" --yes --json
38
- pixel-vibing-cli generate ui-sketch --sketch <file.json> --instruction "<the set, in a line>" --yes --json
36
+ pixel-vibing-cli generate ui --sketch <file.json> --instruction "<the set, in a line>" --yes --json
39
37
  ```
40
38
 
41
39
  Each generation spends the account's credits.
@@ -50,30 +48,23 @@ Each generation spends the account's credits.
50
48
  - No quality is given: the platform draws each asset at the quality it needs. The cost
51
49
  printed is the most it may take, and what it does not use is given back.
52
50
  - `--reference` is a PNG to draw from, repeated for several: at most 8 for an object,
53
- 3 for a creature, a level design, a HUD mockup or a UI sketch, none for a HUD set or icons. A UI
54
- sketch's references are a style to follow (frames, materials, a way of drawing), never what to draw.
51
+ 3 for a creature, a level design or a UI, none for icons. A UI's references are a style
52
+ to follow (frames, materials, a way of drawing), never what to draw.
55
53
  - **Interface art.** When the game shows the player something over the world, make it here
56
54
  rather than drawing it in code:
57
- - `hud` makes a matching set in one generation: `health-bar`, `mana-bar`, `button`,
58
- `inventory-slot`, `panel-frame` and `minimap-frame`, all unless `--pieces` names some.
59
- Frames, buttons, slots and the minimap frame come with nine-slice insets, so stretch
60
- them with the engine's nine-slice sprite rather than scaling them.
61
55
  - `icons` makes up to 16 icons that match, from `--names`: `--purpose skill` for a
62
56
  hotbar or a skill tree (each on a square badge), `--purpose item` for an inventory,
63
57
  potions or loot (each alone). `--size` is 16, 32 (the default) or 64 px.
64
- - `hud-mockup` draws one screen with its whole HUD, to agree on the look first; take
65
- its pieces out in the desktop editor.
66
- - `ui-sketch` draws pieces where a sketch lays them out, when the game needs pieces the
67
- HUD set does not have, or at sizes and places of its own. Write the sketch as JSON:
68
- `{ "canvas": { "width": 256, "height": 256 }, "shapes": [...] }`, the canvas 128 × 128,
69
- 256 × 256, 256 × 128 or 128 × 256, and up to 16 shapes that do not overlap, each
70
- `{ "label", "kind": "rect" | "ellipse" | "polygon", "x", "y", "width", "height" }` in
71
- the canvas's pixels, with `radius` for a rounded rectangle and `sides` (3 to 8) for a
72
- polygon. A shape may say more: `note` (what the piece is, such as "a full red health
73
- bar"), `caption` (text drawn on it, such as "Quit"), `border` (its frame's width in
74
- pixels, which gives the piece nine-slice insets) and `hollow: true` (only the border
75
- drawn, its inside transparent). Leave a few pixels between shapes.
76
- - The art never carries text or numbers, but a UI sketch's captions: draw labels,
58
+ - `ui` draws a game's interface (a HUD, a menu, an inventory) from a layout in JSON:
59
+ a canvas and up to 16 components, each with a `label` (its role, never drawn), a
60
+ `kind` (`rect`, `ellipse` or `polygon`), its position and size (`x`, `y`, `width`,
61
+ `height` in the canvas's pixels) and, as it needs, `caption` (text drawn on it), `note`
62
+ (how it looks), `border` (a frame that stretches by nine-slice) and `hollow` (its inside
63
+ transparent). Read `references/ui.md` before writing one: it gives every field and rule,
64
+ `references/ui.schema.json` is the JSON Schema the file follows (name it with
65
+ `"$schema"`), and `references/ui.example.json` is a whole HUD laid out. Each component
66
+ comes back as its own piece. `ui-sketch` is the same kind by its platform name.
67
+ - The art never carries text or numbers, but a UI's captions: draw labels,
77
68
  counts and values in the game.
78
69
  - **Success.** The asset is pulled at once. The answer's `files` are where its pictures
79
70
  are, relative to `pixelvibing/`.
@@ -92,9 +83,9 @@ pixel-vibing-cli pull <id|name> [--as <name>] --json
92
83
  `creature`, `level-design`, `rotate`, `animate`…
93
84
  - **Pulling.** `pull` adds the asset to the project's art on the platform, where the editor
94
85
  sees it too, and then writes the folder from the platform. An asset of one picture
95
- lands as `<name>`, one of several as `<name>/<n>`; a HUD set's pieces as `ui/hud/<piece>`,
96
- an icon set's as `ui/icon/<name>` and a UI sketch's as `ui/sketch/<label>`, each under
97
- `pixelvibing/out/`. A level design or a HUD mockup is kept as a reference, under
86
+ lands as `<name>`, one of several as `<name>/<n>`; an icon set's pieces as `ui/icon/<name>`
87
+ and a UI's as `ui/sketch/<label>`, each under `pixelvibing/out/`. A level design is kept
88
+ as a reference, under
98
89
  `pixelvibing/ref/`. Pulling again lands nothing while the art still holds it.
99
90
  - **Collisions.** `pull` lands nothing under a name the art holds for something else. When
100
91
  it refuses, give the asset another name with `--as`. A terrain set is landed from the
@@ -129,7 +120,7 @@ pixel-vibing-cli export <default|pixi|phaser|godot> [--out <dir>] --json
129
120
  - **The map.** `map.json` lists each placed sprite, with its frame, its box in world
130
121
  pixels, its level and its layer (`ground`, `object` or `foreground`), in draw order.
131
122
  Place them in that order.
132
- - **Interface art that stretches.** A HUD frame's nine-slice insets travel with it:
123
+ - **Interface art that stretches.** A UI frame's nine-slice insets travel with it:
133
124
  `nineGrid` on its frame in `sheets.json`, `borders` on its PixiJS frame (a
134
125
  `NineSliceSprite` reads them), Phaser's `nine-slices.json` (the widths `nineslice()`
135
126
  takes), and a `NinePatchRect` scene under Godot's `nine-patch/`.
@@ -0,0 +1,12 @@
1
+ {
2
+ "$schema": "./ui.schema.json",
3
+ "canvas": { "width": 256, "height": 256 },
4
+ "shapes": [
5
+ { "label": "Health bar", "kind": "rect", "x": 8, "y": 8, "width": 120, "height": 14, "radius": 2, "border": 2, "note": "a full red health bar" },
6
+ { "label": "Mana bar", "kind": "rect", "x": 8, "y": 28, "width": 120, "height": 14, "radius": 2, "border": 2, "note": "a full blue mana bar" },
7
+ { "label": "Minimap frame", "kind": "ellipse", "x": 176, "y": 8, "width": 72, "height": 72, "border": 6, "hollow": true },
8
+ { "label": "Inventory slot", "kind": "rect", "x": 8, "y": 208, "width": 40, "height": 40, "border": 3, "note": "an empty, recessed slot" },
9
+ { "label": "Skill badge", "kind": "polygon", "sides": 6, "x": 56, "y": 208, "width": 40, "height": 40, "border": 3 },
10
+ { "label": "Quit button", "kind": "rect", "x": 160, "y": 216, "width": 88, "height": 28, "radius": 4, "border": 3, "caption": "Quit" }
11
+ ]
12
+ }
@@ -0,0 +1,63 @@
1
+ # A UI, laid out as JSON
2
+
3
+ `pixel-vibing-cli generate ui --sketch <file.json> --instruction "<the set, in a line>"`
4
+ draws a game's interface from a layout: each shape in the file becomes one piece of the
5
+ interface, drawn where it sits, all of them as one matching set in the game's style. The
6
+ file's shape is `ui.schema.json`, beside this page; `ui.example.json` is a whole HUD laid
7
+ out in it. Name the schema in the file (`"$schema": "./ui.schema.json"`) so an editor checks
8
+ it as it is written; the CLI checks it again, with the platform's own rules, before anything
9
+ is sent or paid for.
10
+
11
+ ## The layout
12
+
13
+ ```json
14
+ {
15
+ "canvas": { "width": 256, "height": 256 },
16
+ "shapes": [
17
+ { "label": "Quit button", "kind": "rect", "x": 160, "y": 216, "width": 88, "height": 28, "radius": 4, "border": 3, "caption": "Quit" }
18
+ ]
19
+ }
20
+ ```
21
+
22
+ - **`canvas`** is 128 × 128, 256 × 256, 256 × 128 or 128 × 256 pixels, the game's own.
23
+ Pick the one the screen's pieces fit at the size they are used in the game.
24
+ - **`shapes`** holds 1 to 16 components. Every one has:
25
+
26
+ | Field | Type | What it is |
27
+ |---|---|---|
28
+ | `label` | text, 1–40 | The component's name and role: `Health bar`, `Quit button`, `Inventory slot`. Read to decide what to draw, never drawn. Unique in the file, ignoring case. |
29
+ | `kind` | `rect` · `ellipse` · `polygon` | Its outline. |
30
+ | `x`, `y` | whole pixels | Its top left corner on the canvas. |
31
+ | `width`, `height` | whole pixels, at least 4 | Its size; it stays inside the canvas. |
32
+
33
+ And, when the component needs it:
34
+
35
+ | Field | Type | What it is |
36
+ |---|---|---|
37
+ | `radius` | whole pixels | A `rect`'s rounded corners. |
38
+ | `sides` | 3–8 | A `polygon`'s sides, its first corner at the top. Required for a polygon. |
39
+ | `caption` | text, up to 24 | Text drawn on the piece, exactly: `Quit`, `Inventory`. The only text the art carries. |
40
+ | `note` | text, up to 200 | What it is or how it looks: `a full red health bar`, `an empty recessed slot`. |
41
+ | `border` | whole pixels, 1 or more | Its frame's width. A piece with a border comes with nine-slice insets, so it stretches; twice the border is less than its shorter side. |
42
+ | `hollow` | `true` | Transparency: only the border is drawn and the inside stays fully transparent, as a minimap frame or a window. Needs `border`. |
43
+
44
+ ## Rules the platform holds it to
45
+
46
+ - Shapes never overlap. Leave 4 or more pixels between them: a piece's outline and
47
+ ornament reach a little past its shape.
48
+ - A label names one component; two shapes with the same label are refused.
49
+ - Nothing else goes in the file: a field the schema does not name is refused.
50
+ - The set's description (`--instruction`, up to 600 characters) says the theme in a line:
51
+ `gothic iron with green enamel`. What each piece is goes in its `label` and `note`.
52
+ - `--reference <png>`, up to 3, is a style to follow, never what to draw.
53
+
54
+ ## Laying out a screen
55
+
56
+ 1. List the components the screen needs: bars, buttons, slots, frames, badges.
57
+ 2. Give each a role-naming `label`, the outline closest to it, and a `note` when the label
58
+ alone does not say how it looks.
59
+ 3. Give frames, buttons and slots a `border`; give a frame the game shows through `hollow`.
60
+ 4. Put a `caption` only on what carries fixed words; counts and values are the game's to draw.
61
+ 5. Place them as the screen places them, so the set is drawn at the sizes it is used at.
62
+
63
+ Each piece is pulled as `ui/sketch/<label>` under `pixelvibing/out/`, its insets with it.
@@ -0,0 +1,79 @@
1
+ {
2
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
3
+ "$id": "https://pixelvibing.com/schemas/ui.schema.json",
4
+ "title": "PixelVibing UI layout",
5
+ "description": "The layout `pixel-vibing-cli generate ui --sketch <file>` reads: a canvas and the labelled shapes on it, each drawn as its own piece of the game's interface where it sits. Coordinates are the canvas's pixels from its top left. Shapes never overlap, and labels are unique, ignoring case. The set's description is given apart, with --instruction.",
6
+ "type": "object",
7
+ "required": ["canvas", "shapes"],
8
+ "additionalProperties": false,
9
+ "properties": {
10
+ "$schema": {
11
+ "type": "string",
12
+ "description": "This schema's path or URL, for an editor; never sent."
13
+ },
14
+ "canvas": {
15
+ "description": "The canvas the UI is drawn on, in the game's pixels.",
16
+ "oneOf": [
17
+ { "$ref": "#/$defs/canvas", "properties": { "width": { "const": 128 }, "height": { "const": 128 } } },
18
+ { "$ref": "#/$defs/canvas", "properties": { "width": { "const": 256 }, "height": { "const": 256 } } },
19
+ { "$ref": "#/$defs/canvas", "properties": { "width": { "const": 256 }, "height": { "const": 128 } } },
20
+ { "$ref": "#/$defs/canvas", "properties": { "width": { "const": 128 }, "height": { "const": 256 } } }
21
+ ]
22
+ },
23
+ "shapes": {
24
+ "type": "array",
25
+ "minItems": 1,
26
+ "maxItems": 16,
27
+ "items": { "$ref": "#/$defs/shape" }
28
+ }
29
+ },
30
+ "$defs": {
31
+ "canvas": {
32
+ "type": "object",
33
+ "required": ["width", "height"],
34
+ "additionalProperties": false,
35
+ "properties": {
36
+ "width": { "type": "integer" },
37
+ "height": { "type": "integer" }
38
+ }
39
+ },
40
+ "shape": {
41
+ "type": "object",
42
+ "required": ["label", "kind", "x", "y", "width", "height"],
43
+ "additionalProperties": false,
44
+ "properties": {
45
+ "label": {
46
+ "type": "string",
47
+ "minLength": 1,
48
+ "maxLength": 40,
49
+ "description": "The component's name, what it is for: health bar, Quit button, inventory slot. Read to decide what to draw; never drawn."
50
+ },
51
+ "kind": {
52
+ "enum": ["rect", "ellipse", "polygon"],
53
+ "description": "The component's outline: a rectangle (with `radius` for rounded corners), an ellipse, or a regular polygon of `sides`."
54
+ },
55
+ "x": { "type": "integer", "minimum": 0, "description": "Left edge, in canvas pixels." },
56
+ "y": { "type": "integer", "minimum": 0, "description": "Top edge, in canvas pixels." },
57
+ "width": { "type": "integer", "minimum": 4, "description": "Width in pixels; x + width is at most the canvas's width." },
58
+ "height": { "type": "integer", "minimum": 4, "description": "Height in pixels; y + height is at most the canvas's height." },
59
+ "radius": { "type": "integer", "minimum": 0, "description": "A rect's corner radius, in pixels." },
60
+ "sides": { "type": "integer", "minimum": 3, "maximum": 8, "description": "A polygon's sides, its first corner at the top. Required for a polygon." },
61
+ "caption": { "type": "string", "maxLength": 24, "description": "Text drawn on the piece, exactly, such as Quit. The only text the art carries." },
62
+ "note": { "type": "string", "maxLength": 200, "description": "What the piece is or looks like, such as a full red health bar." },
63
+ "border": {
64
+ "type": "integer",
65
+ "minimum": 1,
66
+ "description": "The frame's width in pixels, which gives the piece nine-slice insets so it stretches; twice it is less than the shorter side."
67
+ },
68
+ "hollow": {
69
+ "type": "boolean",
70
+ "description": "Transparency: true draws only the border and leaves the inside fully transparent, as a minimap frame or a window. Needs `border`."
71
+ }
72
+ },
73
+ "allOf": [
74
+ { "if": { "properties": { "kind": { "const": "polygon" } } }, "then": { "required": ["sides"] } },
75
+ { "if": { "properties": { "hollow": { "const": true } }, "required": ["hollow"] }, "then": { "required": ["border"] } }
76
+ ]
77
+ }
78
+ }
79
+ }