@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.
package/dist/pixel-vibing-cli.js
CHANGED
|
@@ -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
|
-
"
|
|
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
|
|
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
|
|
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
|
|
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,
|
|
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 ${
|
|
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 === "
|
|
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 ["
|
|
4037
|
-
if (stringOf(args, flag) !== void 0) throw new CliError(`--${flag} is not a UI
|
|
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
|
|
4039
|
+
if (stringOf(args, "sketch") !== void 0) throw new CliError("--sketch is a UI's");
|
|
4049
4040
|
if (set) {
|
|
4050
|
-
const { fields, words: words2 } =
|
|
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 ["
|
|
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,
|
|
4104
|
-
usage: "<object|creature|level-design|
|
|
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;
|
|
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
|
|
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.
|
|
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
|
-
|
|
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
|
|
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 (
|
|
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.
|
|
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
|
|
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
|
|
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
|
|
54
|
-
|
|
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
|
-
- `
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
`
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
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>`;
|
|
96
|
-
|
|
97
|
-
|
|
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
|
|
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
|
+
}
|