picopilot 0.0.0 → 0.1.1
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/LICENSE +661 -0
- package/dist/bin.d.ts +3 -0
- package/dist/bin.d.ts.map +1 -0
- package/dist/bin.js +4 -0
- package/dist/bin.js.map +1 -0
- package/dist/cli.d.ts +15 -0
- package/dist/cli.d.ts.map +1 -0
- package/dist/cli.js +59 -0
- package/dist/cli.js.map +1 -0
- package/dist/commands/audio.d.ts +17 -0
- package/dist/commands/audio.d.ts.map +1 -0
- package/dist/commands/audio.js +315 -0
- package/dist/commands/audio.js.map +1 -0
- package/dist/commands/export.d.ts +36 -0
- package/dist/commands/export.d.ts.map +1 -0
- package/dist/commands/export.js +257 -0
- package/dist/commands/export.js.map +1 -0
- package/dist/commands/gfx.d.ts +28 -0
- package/dist/commands/gfx.d.ts.map +1 -0
- package/dist/commands/gfx.js +521 -0
- package/dist/commands/gfx.js.map +1 -0
- package/dist/commands/init.d.ts +34 -0
- package/dist/commands/init.d.ts.map +1 -0
- package/dist/commands/init.js +151 -0
- package/dist/commands/init.js.map +1 -0
- package/dist/commands/lint.d.ts +20 -0
- package/dist/commands/lint.d.ts.map +1 -0
- package/dist/commands/lint.js +90 -0
- package/dist/commands/lint.js.map +1 -0
- package/dist/commands/minify.d.ts +20 -0
- package/dist/commands/minify.d.ts.map +1 -0
- package/dist/commands/minify.js +129 -0
- package/dist/commands/minify.js.map +1 -0
- package/dist/commands/music.d.ts +13 -0
- package/dist/commands/music.d.ts.map +1 -0
- package/dist/commands/music.js +241 -0
- package/dist/commands/music.js.map +1 -0
- package/dist/commands/playtest.d.ts +29 -0
- package/dist/commands/playtest.d.ts.map +1 -0
- package/dist/commands/playtest.js +631 -0
- package/dist/commands/playtest.js.map +1 -0
- package/dist/commands/run.d.ts +30 -0
- package/dist/commands/run.d.ts.map +1 -0
- package/dist/commands/run.js +224 -0
- package/dist/commands/run.js.map +1 -0
- package/dist/commands/serve.d.ts +50 -0
- package/dist/commands/serve.d.ts.map +1 -0
- package/dist/commands/serve.js +213 -0
- package/dist/commands/serve.js.map +1 -0
- package/dist/commands/sfx.d.ts +13 -0
- package/dist/commands/sfx.d.ts.map +1 -0
- package/dist/commands/sfx.js +254 -0
- package/dist/commands/sfx.js.map +1 -0
- package/dist/commands/tokens.d.ts +36 -0
- package/dist/commands/tokens.d.ts.map +1 -0
- package/dist/commands/tokens.js +129 -0
- package/dist/commands/tokens.js.map +1 -0
- package/dist/commands/verify.d.ts +57 -0
- package/dist/commands/verify.d.ts.map +1 -0
- package/dist/commands/verify.js +213 -0
- package/dist/commands/verify.js.map +1 -0
- package/dist/commands/version.d.ts +12 -0
- package/dist/commands/version.d.ts.map +1 -0
- package/dist/commands/version.js +24 -0
- package/dist/commands/version.js.map +1 -0
- package/dist/engine/audio/index.d.ts +14 -0
- package/dist/engine/audio/index.d.ts.map +1 -0
- package/dist/engine/audio/index.js +14 -0
- package/dist/engine/audio/index.js.map +1 -0
- package/dist/engine/audio/music.d.ts +116 -0
- package/dist/engine/audio/music.d.ts.map +1 -0
- package/dist/engine/audio/music.js +138 -0
- package/dist/engine/audio/music.js.map +1 -0
- package/dist/engine/audio/section.d.ts +34 -0
- package/dist/engine/audio/section.d.ts.map +1 -0
- package/dist/engine/audio/section.js +67 -0
- package/dist/engine/audio/section.js.map +1 -0
- package/dist/engine/audio/sfx.d.ts +141 -0
- package/dist/engine/audio/sfx.d.ts.map +1 -0
- package/dist/engine/audio/sfx.js +506 -0
- package/dist/engine/audio/sfx.js.map +1 -0
- package/dist/engine/cart/cart.d.ts +75 -0
- package/dist/engine/cart/cart.d.ts.map +1 -0
- package/dist/engine/cart/cart.js +153 -0
- package/dist/engine/cart/cart.js.map +1 -0
- package/dist/engine/cart/errors.d.ts +29 -0
- package/dist/engine/cart/errors.d.ts.map +1 -0
- package/dist/engine/cart/errors.js +26 -0
- package/dist/engine/cart/errors.js.map +1 -0
- package/dist/engine/cart/gfx.d.ts +79 -0
- package/dist/engine/cart/gfx.d.ts.map +1 -0
- package/dist/engine/cart/gfx.js +171 -0
- package/dist/engine/cart/gfx.js.map +1 -0
- package/dist/engine/cart/index.d.ts +15 -0
- package/dist/engine/cart/index.d.ts.map +1 -0
- package/dist/engine/cart/index.js +15 -0
- package/dist/engine/cart/index.js.map +1 -0
- package/dist/engine/cart/map.d.ts +46 -0
- package/dist/engine/cart/map.d.ts.map +1 -0
- package/dist/engine/cart/map.js +118 -0
- package/dist/engine/cart/map.js.map +1 -0
- package/dist/engine/config/config.d.ts +74 -0
- package/dist/engine/config/config.d.ts.map +1 -0
- package/dist/engine/config/config.js +103 -0
- package/dist/engine/config/config.js.map +1 -0
- package/dist/engine/config/index.d.ts +7 -0
- package/dist/engine/config/index.d.ts.map +1 -0
- package/dist/engine/config/index.js +7 -0
- package/dist/engine/config/index.js.map +1 -0
- package/dist/engine/gfx/grid.d.ts +51 -0
- package/dist/engine/gfx/grid.d.ts.map +1 -0
- package/dist/engine/gfx/grid.js +116 -0
- package/dist/engine/gfx/grid.js.map +1 -0
- package/dist/engine/gfx/index.d.ts +17 -0
- package/dist/engine/gfx/index.d.ts.map +1 -0
- package/dist/engine/gfx/index.js +17 -0
- package/dist/engine/gfx/index.js.map +1 -0
- package/dist/engine/gfx/label.d.ts +34 -0
- package/dist/engine/gfx/label.d.ts.map +1 -0
- package/dist/engine/gfx/label.js +190 -0
- package/dist/engine/gfx/label.js.map +1 -0
- package/dist/engine/gfx/overlap.d.ts +42 -0
- package/dist/engine/gfx/overlap.d.ts.map +1 -0
- package/dist/engine/gfx/overlap.js +57 -0
- package/dist/engine/gfx/overlap.js.map +1 -0
- package/dist/engine/gfx/palette.d.ts +29 -0
- package/dist/engine/gfx/palette.d.ts.map +1 -0
- package/dist/engine/gfx/palette.js +46 -0
- package/dist/engine/gfx/palette.js.map +1 -0
- package/dist/engine/gfx/png.d.ts +31 -0
- package/dist/engine/gfx/png.d.ts.map +1 -0
- package/dist/engine/gfx/png.js +165 -0
- package/dist/engine/gfx/png.js.map +1 -0
- package/dist/engine/gfx/render.d.ts +42 -0
- package/dist/engine/gfx/render.d.ts.map +1 -0
- package/dist/engine/gfx/render.js +72 -0
- package/dist/engine/gfx/render.js.map +1 -0
- package/dist/engine/index.d.ts +22 -0
- package/dist/engine/index.d.ts.map +1 -0
- package/dist/engine/index.js +22 -0
- package/dist/engine/index.js.map +1 -0
- package/dist/engine/pico8/adapter.d.ts +267 -0
- package/dist/engine/pico8/adapter.d.ts.map +1 -0
- package/dist/engine/pico8/adapter.js +37 -0
- package/dist/engine/pico8/adapter.js.map +1 -0
- package/dist/engine/pico8/drive.d.ts +308 -0
- package/dist/engine/pico8/drive.d.ts.map +1 -0
- package/dist/engine/pico8/drive.js +581 -0
- package/dist/engine/pico8/drive.js.map +1 -0
- package/dist/engine/pico8/harness.d.ts +146 -0
- package/dist/engine/pico8/harness.d.ts.map +1 -0
- package/dist/engine/pico8/harness.js +251 -0
- package/dist/engine/pico8/harness.js.map +1 -0
- package/dist/engine/pico8/index.d.ts +21 -0
- package/dist/engine/pico8/index.d.ts.map +1 -0
- package/dist/engine/pico8/index.js +21 -0
- package/dist/engine/pico8/index.js.map +1 -0
- package/dist/engine/pico8/sentinel.d.ts +63 -0
- package/dist/engine/pico8/sentinel.d.ts.map +1 -0
- package/dist/engine/pico8/sentinel.js +86 -0
- package/dist/engine/pico8/sentinel.js.map +1 -0
- package/dist/engine/pico8/session-daemon-entry.d.ts +10 -0
- package/dist/engine/pico8/session-daemon-entry.d.ts.map +1 -0
- package/dist/engine/pico8/session-daemon-entry.js +11 -0
- package/dist/engine/pico8/session-daemon-entry.js.map +1 -0
- package/dist/engine/pico8/session-daemon-main.d.ts +44 -0
- package/dist/engine/pico8/session-daemon-main.d.ts.map +1 -0
- package/dist/engine/pico8/session-daemon-main.js +71 -0
- package/dist/engine/pico8/session-daemon-main.js.map +1 -0
- package/dist/engine/pico8/session-daemon.d.ts +68 -0
- package/dist/engine/pico8/session-daemon.d.ts.map +1 -0
- package/dist/engine/pico8/session-daemon.js +264 -0
- package/dist/engine/pico8/session-daemon.js.map +1 -0
- package/dist/engine/pico8/session.d.ts +140 -0
- package/dist/engine/pico8/session.d.ts.map +1 -0
- package/dist/engine/pico8/session.js +250 -0
- package/dist/engine/pico8/session.js.map +1 -0
- package/dist/engine/pico8/shell.d.ts +103 -0
- package/dist/engine/pico8/shell.d.ts.map +1 -0
- package/dist/engine/pico8/shell.js +324 -0
- package/dist/engine/pico8/shell.js.map +1 -0
- package/dist/engine/pico8/supervisor.d.ts +137 -0
- package/dist/engine/pico8/supervisor.d.ts.map +1 -0
- package/dist/engine/pico8/supervisor.js +140 -0
- package/dist/engine/pico8/supervisor.js.map +1 -0
- package/dist/engine/scaffold/index.d.ts +8 -0
- package/dist/engine/scaffold/index.d.ts.map +1 -0
- package/dist/engine/scaffold/index.js +8 -0
- package/dist/engine/scaffold/index.js.map +1 -0
- package/dist/engine/scaffold/scaffold.d.ts +49 -0
- package/dist/engine/scaffold/scaffold.d.ts.map +1 -0
- package/dist/engine/scaffold/scaffold.js +259 -0
- package/dist/engine/scaffold/scaffold.js.map +1 -0
- package/dist/engine/shrinko/adapter.d.ts +167 -0
- package/dist/engine/shrinko/adapter.d.ts.map +1 -0
- package/dist/engine/shrinko/adapter.js +107 -0
- package/dist/engine/shrinko/adapter.js.map +1 -0
- package/dist/engine/shrinko/index.d.ts +13 -0
- package/dist/engine/shrinko/index.d.ts.map +1 -0
- package/dist/engine/shrinko/index.js +13 -0
- package/dist/engine/shrinko/index.js.map +1 -0
- package/dist/engine/shrinko/shell.d.ts +69 -0
- package/dist/engine/shrinko/shell.d.ts.map +1 -0
- package/dist/engine/shrinko/shell.js +156 -0
- package/dist/engine/shrinko/shell.js.map +1 -0
- package/dist/engine/skills/index.d.ts +8 -0
- package/dist/engine/skills/index.d.ts.map +1 -0
- package/dist/engine/skills/index.js +8 -0
- package/dist/engine/skills/index.js.map +1 -0
- package/dist/engine/skills/skills.d.ts +104 -0
- package/dist/engine/skills/skills.d.ts.map +1 -0
- package/dist/engine/skills/skills.js +149 -0
- package/dist/engine/skills/skills.js.map +1 -0
- package/dist/index.d.ts +10 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +10 -0
- package/dist/index.js.map +1 -0
- package/dist/version.d.ts +2 -0
- package/dist/version.d.ts.map +1 -0
- package/dist/version.js +19 -0
- package/dist/version.js.map +1 -0
- package/package.json +48 -2
- package/src/bin.ts +4 -0
- package/src/cli.ts +61 -0
- package/src/commands/audio.test.ts +280 -0
- package/src/commands/audio.ts +410 -0
- package/src/commands/export.test.ts +310 -0
- package/src/commands/export.ts +316 -0
- package/src/commands/gfx-io.test.ts +178 -0
- package/src/commands/gfx.test.ts +470 -0
- package/src/commands/gfx.ts +645 -0
- package/src/commands/init.test.ts +482 -0
- package/src/commands/init.ts +193 -0
- package/src/commands/lint.test.ts +137 -0
- package/src/commands/lint.ts +117 -0
- package/src/commands/minify.test.ts +166 -0
- package/src/commands/minify.ts +156 -0
- package/src/commands/music.test.ts +272 -0
- package/src/commands/music.ts +288 -0
- package/src/commands/playtest.test.ts +317 -0
- package/src/commands/playtest.ts +773 -0
- package/src/commands/run.test.ts +264 -0
- package/src/commands/run.ts +285 -0
- package/src/commands/serve.test.ts +227 -0
- package/src/commands/serve.ts +272 -0
- package/src/commands/sfx.test.ts +349 -0
- package/src/commands/sfx.ts +300 -0
- package/src/commands/tokens.test.ts +212 -0
- package/src/commands/tokens.ts +168 -0
- package/src/commands/verify.test.ts +238 -0
- package/src/commands/verify.ts +256 -0
- package/src/commands/version.test.ts +70 -0
- package/src/commands/version.ts +24 -0
- package/src/engine/audio/index.ts +47 -0
- package/src/engine/audio/music.test.ts +188 -0
- package/src/engine/audio/music.ts +218 -0
- package/src/engine/audio/section.test.ts +124 -0
- package/src/engine/audio/section.ts +79 -0
- package/src/engine/audio/sfx.test.ts +410 -0
- package/src/engine/audio/sfx.ts +694 -0
- package/src/engine/cart/cart.test.ts +304 -0
- package/src/engine/cart/cart.ts +211 -0
- package/src/engine/cart/errors.ts +39 -0
- package/src/engine/cart/fixtures/crlf.p8 +6 -0
- package/src/engine/cart/fixtures/full.p8 +174 -0
- package/src/engine/cart/fixtures/header-only.p8 +2 -0
- package/src/engine/cart/fixtures/malformed-no-header.p8 +2 -0
- package/src/engine/cart/fixtures/minimal.p8 +10 -0
- package/src/engine/cart/fixtures/no-trailing-newline.p8 +6 -0
- package/src/engine/cart/gfx.ts +195 -0
- package/src/engine/cart/index.ts +27 -0
- package/src/engine/cart/map.ts +134 -0
- package/src/engine/config/config.ts +115 -0
- package/src/engine/config/index.ts +12 -0
- package/src/engine/gfx/grid.test.ts +128 -0
- package/src/engine/gfx/grid.ts +136 -0
- package/src/engine/gfx/index.ts +34 -0
- package/src/engine/gfx/label.test.ts +105 -0
- package/src/engine/gfx/label.ts +229 -0
- package/src/engine/gfx/overlap.test.ts +71 -0
- package/src/engine/gfx/overlap.ts +66 -0
- package/src/engine/gfx/palette.test.ts +35 -0
- package/src/engine/gfx/palette.ts +54 -0
- package/src/engine/gfx/png.test.ts +111 -0
- package/src/engine/gfx/png.ts +196 -0
- package/src/engine/gfx/render.test.ts +180 -0
- package/src/engine/gfx/render.ts +106 -0
- package/src/engine/index.ts +21 -0
- package/src/engine/pico8/adapter.ts +303 -0
- package/src/engine/pico8/drive.test.ts +415 -0
- package/src/engine/pico8/drive.ts +729 -0
- package/src/engine/pico8/harness.test.ts +181 -0
- package/src/engine/pico8/harness.ts +354 -0
- package/src/engine/pico8/index.ts +20 -0
- package/src/engine/pico8/sentinel.test.ts +80 -0
- package/src/engine/pico8/sentinel.ts +93 -0
- package/src/engine/pico8/session-daemon-entry.ts +12 -0
- package/src/engine/pico8/session-daemon-main.ts +98 -0
- package/src/engine/pico8/session-daemon.test.ts +212 -0
- package/src/engine/pico8/session-daemon.ts +306 -0
- package/src/engine/pico8/session.test.ts +239 -0
- package/src/engine/pico8/session.ts +321 -0
- package/src/engine/pico8/shell.test.ts +583 -0
- package/src/engine/pico8/shell.ts +412 -0
- package/src/engine/pico8/supervisor.test.ts +105 -0
- package/src/engine/pico8/supervisor.ts +197 -0
- package/src/engine/scaffold/index.ts +15 -0
- package/src/engine/scaffold/scaffold.ts +278 -0
- package/src/engine/shrinko/adapter.ts +241 -0
- package/src/engine/shrinko/fixtures/count-no-compressed.txt +2 -0
- package/src/engine/shrinko/fixtures/count-over-budget.txt +3 -0
- package/src/engine/shrinko/fixtures/count-under-budget.txt +3 -0
- package/src/engine/shrinko/index.ts +33 -0
- package/src/engine/shrinko/shell.ts +224 -0
- package/src/engine/shrinko/shrinko.test.ts +182 -0
- package/src/engine/skills/index.ts +14 -0
- package/src/engine/skills/skills.test.ts +376 -0
- package/src/engine/skills/skills.ts +205 -0
- package/src/index.ts +13 -0
- package/src/skills/game-design-reference/SKILL.md +70 -0
- package/src/skills/game-jam/SKILL.md +45 -0
- package/src/skills/picopilot-art/SKILL.md +62 -0
- package/src/skills/picopilot-audio/SKILL.md +194 -0
- package/src/skills/picopilot-code/SKILL.md +97 -0
- package/src/skills/picopilot-code/reference/README.md +42 -0
- package/src/skills/picopilot-code/reference/mode7-racing.md +95 -0
- package/src/skills/picopilot-code/reference/platformer.md +119 -0
- package/src/skills/picopilot-code/reference/puzzle-grid.md +106 -0
- package/src/skills/picopilot-code/reference/rpg-menus-dialog.md +122 -0
- package/src/skills/picopilot-code/reference/top-down-adventure.md +117 -0
- package/src/skills/picopilot-code/reference/twin-stick-arcade.md +128 -0
- package/src/skills/picopilot-debug/SKILL.md +195 -0
- package/src/skills/picopilot-overview/SKILL.md +52 -0
- package/src/version.ts +22 -0
|
@@ -0,0 +1,645 @@
|
|
|
1
|
+
import {existsSync, readFileSync, writeFileSync} from 'node:fs';
|
|
2
|
+
import {basename, dirname, extname, isAbsolute, join, resolve} from 'node:path';
|
|
3
|
+
import {Cli, z} from 'incur';
|
|
4
|
+
|
|
5
|
+
import {
|
|
6
|
+
Cart,
|
|
7
|
+
CartParseError,
|
|
8
|
+
GFX_WIDTH,
|
|
9
|
+
GfxSheet,
|
|
10
|
+
SPRITE_SIZE,
|
|
11
|
+
spriteAliasesMap,
|
|
12
|
+
} from '../engine/cart/index.js';
|
|
13
|
+
import {
|
|
14
|
+
decideOverlap,
|
|
15
|
+
GfxGridError,
|
|
16
|
+
gridToNibbles,
|
|
17
|
+
nibblesToGrid,
|
|
18
|
+
renderSheetPng,
|
|
19
|
+
renderSpritePng,
|
|
20
|
+
SHEET_RENDER_SCALE,
|
|
21
|
+
SPRITE_RENDER_SCALE,
|
|
22
|
+
} from '../engine/gfx/index.js';
|
|
23
|
+
import {
|
|
24
|
+
type ShrinkoAdapter,
|
|
25
|
+
ShellShrinkoAdapter,
|
|
26
|
+
} from '../engine/shrinko/index.js';
|
|
27
|
+
|
|
28
|
+
/** Injects the shrinko adapter `gfx import`/`gfx export` use (the spritesheet round-trip). */
|
|
29
|
+
export type ShrinkoAdapterFactory = (env: NodeJS.ProcessEnv) => ShrinkoAdapter;
|
|
30
|
+
|
|
31
|
+
const defaultShrinkoFactory: ShrinkoAdapterFactory = (env) =>
|
|
32
|
+
new ShellShrinkoAdapter({env});
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* The exit code for the `map-overlap` refusal. Chosen as 1 (a plain failure),
|
|
36
|
+
* matching every other picopilot structured refusal (`cart-not-found`,
|
|
37
|
+
* `already-initialised`, `verify-failed`): the refusal is a "you asked for
|
|
38
|
+
* something I won't do" failure, not a distinct capability-gap the way `verify`'s
|
|
39
|
+
* `gate-incapable` (exit 2) is. What makes it safe is not a special exit code
|
|
40
|
+
* but that it is NON-ZERO (an agent must not read it as done) AND the sprite's
|
|
41
|
+
* bytes are left untouched.
|
|
42
|
+
*/
|
|
43
|
+
export const MAP_OVERLAP_EXIT = 1;
|
|
44
|
+
|
|
45
|
+
/** Resolves a cart argument to an absolute path (relative to cwd). */
|
|
46
|
+
function resolveCart(cart: string): string {
|
|
47
|
+
return isAbsolute(cart) ? cart : resolve(process.cwd(), cart);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* The default PNG output path for a `gfx render`: alongside the cart, named
|
|
52
|
+
* `<cart-stem>-sprite-<n>.png` or `<cart-stem>-sheet.png`. Predictable + stable
|
|
53
|
+
* so an agent can render → look → fix → re-render at the SAME path without
|
|
54
|
+
* juggling filenames.
|
|
55
|
+
*/
|
|
56
|
+
function defaultRenderPath(cartPath: string, target: 'sheet' | number): string {
|
|
57
|
+
const stem = basename(cartPath, extname(cartPath));
|
|
58
|
+
const suffix = target === 'sheet' ? 'sheet' : `sprite-${target}`;
|
|
59
|
+
return join(dirname(cartPath), `${stem}-${suffix}.png`);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Loads + parses a cart from `cartPath`, returning either the parsed cart and
|
|
64
|
+
* its source text or a structured reason the caller turns into an `error(...)`.
|
|
65
|
+
* Keeps the fs + parse boilerplate out of both command handlers.
|
|
66
|
+
*/
|
|
67
|
+
function loadCart(
|
|
68
|
+
cartPath: string,
|
|
69
|
+
):
|
|
70
|
+
| {ok: true; cart: Cart; text: string}
|
|
71
|
+
| {ok: false; code: string; message: string} {
|
|
72
|
+
if (!existsSync(cartPath)) {
|
|
73
|
+
return {
|
|
74
|
+
ok: false,
|
|
75
|
+
code: 'cart-not-found',
|
|
76
|
+
message: `no cart at ${cartPath}`,
|
|
77
|
+
};
|
|
78
|
+
}
|
|
79
|
+
const text = readFileSync(cartPath, 'utf8');
|
|
80
|
+
try {
|
|
81
|
+
return {ok: true, cart: Cart.parse(text), text};
|
|
82
|
+
} catch (e) {
|
|
83
|
+
if (e instanceof CartParseError) {
|
|
84
|
+
return {
|
|
85
|
+
ok: false,
|
|
86
|
+
code: 'cart-parse-failed',
|
|
87
|
+
message: `cart at ${cartPath} does not parse (${e.code}): ${e.message}`,
|
|
88
|
+
};
|
|
89
|
+
}
|
|
90
|
+
throw e;
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* Builds the `gfx` command group: `gfx show` (read a sprite as a char grid),
|
|
96
|
+
* `gfx set` (write a char grid back into `__gfx__`, with the map-overlap
|
|
97
|
+
* smart-refuse), and `gfx render` (upscaled palette-accurate PNG, the JUDGE
|
|
98
|
+
* surface). Returned as its own `Cli` so it mounts under the root as a
|
|
99
|
+
* group, which also gives the `gfx.set.options.allowMapOverlap` config path its
|
|
100
|
+
* `picopilot.json` reads through (incur keys config by command path).
|
|
101
|
+
*/
|
|
102
|
+
function buildGfxGroup(shrinkoFactory: ShrinkoAdapterFactory): Cli.Cli {
|
|
103
|
+
const gfx = Cli.create('gfx', {
|
|
104
|
+
description:
|
|
105
|
+
'Read and write sprites as a readable char grid (the pixel-art EDIT surface).',
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
gfx.command('show', {
|
|
109
|
+
description:
|
|
110
|
+
'Render a sprite from __gfx__ into a readable char grid (. = transparent, 0-f = the 16 colours). CTAs to `gfx set`.',
|
|
111
|
+
args: z.object({
|
|
112
|
+
sprite: z.coerce
|
|
113
|
+
.number()
|
|
114
|
+
.int()
|
|
115
|
+
.min(0)
|
|
116
|
+
.max(255)
|
|
117
|
+
.describe('The sprite index 0-255 to render.'),
|
|
118
|
+
cart: z
|
|
119
|
+
.string()
|
|
120
|
+
.default('main.p8')
|
|
121
|
+
.describe(
|
|
122
|
+
'The .p8 cart to read. Defaults to main.p8 in the current folder.',
|
|
123
|
+
),
|
|
124
|
+
}),
|
|
125
|
+
output: z.object({
|
|
126
|
+
sprite: z.number().describe('The sprite index that was rendered.'),
|
|
127
|
+
grid: z
|
|
128
|
+
.string()
|
|
129
|
+
.describe(
|
|
130
|
+
'The char grid: 8 lines of 8 chars, . = transparent, 0-f = colours 1-15.',
|
|
131
|
+
),
|
|
132
|
+
aliasesMap: z
|
|
133
|
+
.boolean()
|
|
134
|
+
.describe(
|
|
135
|
+
'True for sprites 128-255, which alias the shared map region (see `gfx set`).',
|
|
136
|
+
),
|
|
137
|
+
}),
|
|
138
|
+
examples: [
|
|
139
|
+
{description: 'Show sprite 1 of main.p8', args: {sprite: 1}},
|
|
140
|
+
{
|
|
141
|
+
description: 'Show sprite 3 of a specific cart',
|
|
142
|
+
args: {sprite: 3, cart: 'game.p8'},
|
|
143
|
+
},
|
|
144
|
+
],
|
|
145
|
+
run({args, error, ok}) {
|
|
146
|
+
const cartPath = resolveCart(args.cart);
|
|
147
|
+
const loaded = loadCart(cartPath);
|
|
148
|
+
if (!loaded.ok) {
|
|
149
|
+
return error({code: loaded.code, message: loaded.message, exitCode: 1});
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
const sheet = GfxSheet.fromBody(loaded.cart.getSection('gfx'));
|
|
153
|
+
const grid = nibblesToGrid(sheet.getSprite(args.sprite));
|
|
154
|
+
|
|
155
|
+
// The read → fix → write loop: `gfx show` always CTAs to `gfx set` so an
|
|
156
|
+
// agent that sees a broken sprite is led straight to the edit surface.
|
|
157
|
+
return ok(
|
|
158
|
+
{
|
|
159
|
+
sprite: args.sprite,
|
|
160
|
+
grid,
|
|
161
|
+
aliasesMap: spriteAliasesMap(args.sprite),
|
|
162
|
+
},
|
|
163
|
+
{
|
|
164
|
+
cta: {
|
|
165
|
+
description: 'Edit the grid, then write it back with:',
|
|
166
|
+
commands: [
|
|
167
|
+
{
|
|
168
|
+
command: `gfx set ${args.sprite}`,
|
|
169
|
+
description: 'Write an edited char grid back into __gfx__.',
|
|
170
|
+
},
|
|
171
|
+
],
|
|
172
|
+
},
|
|
173
|
+
},
|
|
174
|
+
);
|
|
175
|
+
},
|
|
176
|
+
});
|
|
177
|
+
|
|
178
|
+
gfx.command('set', {
|
|
179
|
+
description:
|
|
180
|
+
'Write a char grid back into a sprite in __gfx__. Sprites 128-255 alias the shared map region: a write that would clobber existing data is refused unless authorised (--allow-map-overlap / allowMapOverlap config).',
|
|
181
|
+
args: z.object({
|
|
182
|
+
sprite: z.coerce
|
|
183
|
+
.number()
|
|
184
|
+
.int()
|
|
185
|
+
.min(0)
|
|
186
|
+
.max(255)
|
|
187
|
+
.describe('The sprite index 0-255 to write.'),
|
|
188
|
+
grid: z
|
|
189
|
+
.string()
|
|
190
|
+
.describe(
|
|
191
|
+
'The char grid: 8 lines of 8 chars, . = transparent, 0-f = colours 1-15.',
|
|
192
|
+
),
|
|
193
|
+
cart: z
|
|
194
|
+
.string()
|
|
195
|
+
.default('main.p8')
|
|
196
|
+
.describe(
|
|
197
|
+
'The .p8 cart to write. Defaults to main.p8 in the current folder.',
|
|
198
|
+
),
|
|
199
|
+
}),
|
|
200
|
+
options: z.object({
|
|
201
|
+
allowMapOverlap: z
|
|
202
|
+
.boolean()
|
|
203
|
+
.default(false)
|
|
204
|
+
.describe(
|
|
205
|
+
'Authorise overwriting existing shared-region data when writing sprites 128-255 (ADR-0004). Also settable as allowMapOverlap in picopilot.json.',
|
|
206
|
+
),
|
|
207
|
+
}),
|
|
208
|
+
output: z.object({
|
|
209
|
+
sprite: z.number().describe('The sprite index that was written.'),
|
|
210
|
+
cart: z
|
|
211
|
+
.string()
|
|
212
|
+
.describe('The absolute path of the cart that was written.'),
|
|
213
|
+
aliasesMap: z
|
|
214
|
+
.boolean()
|
|
215
|
+
.describe(
|
|
216
|
+
'True for sprites 128-255, which alias the shared map region.',
|
|
217
|
+
),
|
|
218
|
+
overwroteSharedData: z
|
|
219
|
+
.boolean()
|
|
220
|
+
.describe(
|
|
221
|
+
'True when this write overwrote existing shared-region data (authorised).',
|
|
222
|
+
),
|
|
223
|
+
note: z
|
|
224
|
+
.string()
|
|
225
|
+
.optional()
|
|
226
|
+
.describe(
|
|
227
|
+
'An advisory note (e.g. the shared-region aliasing) when relevant.',
|
|
228
|
+
),
|
|
229
|
+
}),
|
|
230
|
+
examples: [
|
|
231
|
+
{
|
|
232
|
+
description: 'Write an edited grid into sprite 1',
|
|
233
|
+
args: {
|
|
234
|
+
sprite: 1,
|
|
235
|
+
grid: '........\n........\n........\n........\n........\n........\n........\n........',
|
|
236
|
+
},
|
|
237
|
+
},
|
|
238
|
+
],
|
|
239
|
+
run({args, options, error, ok}) {
|
|
240
|
+
const cartPath = resolveCart(args.cart);
|
|
241
|
+
|
|
242
|
+
// Decode the incoming grid FIRST: a malformed grid is a caller error and
|
|
243
|
+
// must never touch the cart. (No bytes change on a bad grid.)
|
|
244
|
+
let nibbles: number[][];
|
|
245
|
+
try {
|
|
246
|
+
nibbles = gridToNibbles(args.grid);
|
|
247
|
+
} catch (e) {
|
|
248
|
+
if (e instanceof GfxGridError) {
|
|
249
|
+
return error({
|
|
250
|
+
code: 'invalid-grid',
|
|
251
|
+
message: `the char grid is malformed: ${e.message}`,
|
|
252
|
+
exitCode: 1,
|
|
253
|
+
});
|
|
254
|
+
}
|
|
255
|
+
throw e;
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
const loaded = loadCart(cartPath);
|
|
259
|
+
if (!loaded.ok) {
|
|
260
|
+
return error({code: loaded.code, message: loaded.message, exitCode: 1});
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
const sheet = GfxSheet.fromBody(loaded.cart.getSection('gfx'));
|
|
264
|
+
|
|
265
|
+
// The overlap decision reads the target sprite's CURRENT __gfx__ pixels
|
|
266
|
+
// (the shared-bank bytes at risk), NOT __map__ (ADR-0004). The flag and
|
|
267
|
+
// the picopilot.json config both feed `options.allowMapOverlap` (incur
|
|
268
|
+
// resolves argv > config > default), so both authorisation paths converge
|
|
269
|
+
// here.
|
|
270
|
+
const decision = decideOverlap(
|
|
271
|
+
sheet,
|
|
272
|
+
args.sprite,
|
|
273
|
+
options.allowMapOverlap,
|
|
274
|
+
);
|
|
275
|
+
|
|
276
|
+
if (decision.kind === 'refused') {
|
|
277
|
+
// The silent-corruption guard: REFUSE and DO NOT write. The cart file is
|
|
278
|
+
// never opened for writing on this path, so the sprite's __gfx__ bytes
|
|
279
|
+
// stay byte-identical. Structured envelope (code + detail + remedy) with
|
|
280
|
+
// a nonzero exit so an agent stops rather than treating exit-0 as done.
|
|
281
|
+
return error({
|
|
282
|
+
code: 'map-overlap',
|
|
283
|
+
message: `sprite ${args.sprite} aliases the shared map region and its current __gfx__ pixels hold data (${decision.nonZeroPixels} non-zero); refusing to silently overwrite it. Remedy: pass --allow-map-overlap, set allowMapOverlap in picopilot.json, or target a sprite 0-127.`,
|
|
284
|
+
exitCode: MAP_OVERLAP_EXIT,
|
|
285
|
+
cta: {
|
|
286
|
+
description: 'To proceed anyway (this overwrites the shared data):',
|
|
287
|
+
commands: [
|
|
288
|
+
{
|
|
289
|
+
command: `gfx set ${args.sprite} --allow-map-overlap`,
|
|
290
|
+
description: 'Re-run authorising the shared-region overwrite.',
|
|
291
|
+
},
|
|
292
|
+
],
|
|
293
|
+
},
|
|
294
|
+
});
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
// Allowed: write the sprite and persist. Preserve every other section by
|
|
298
|
+
// committing through the cart model (only __gfx__ changes).
|
|
299
|
+
sheet.setSprite(args.sprite, nibbles);
|
|
300
|
+
sheet.commit(loaded.cart);
|
|
301
|
+
writeFileSync(cartPath, loaded.cart.serialize());
|
|
302
|
+
|
|
303
|
+
const overwroteSharedData =
|
|
304
|
+
decision.kind === 'allowed-shared' && decision.reason === 'authorised';
|
|
305
|
+
const note =
|
|
306
|
+
decision.kind === 'allowed-shared'
|
|
307
|
+
? decision.reason === 'authorised'
|
|
308
|
+
? `sprite ${args.sprite} aliases the shared map region; overwrote existing shared data (authorised).`
|
|
309
|
+
: `sprite ${args.sprite} aliases the shared map region; it was empty, so the write is safe.`
|
|
310
|
+
: undefined;
|
|
311
|
+
|
|
312
|
+
return ok({
|
|
313
|
+
sprite: args.sprite,
|
|
314
|
+
cart: cartPath,
|
|
315
|
+
aliasesMap: spriteAliasesMap(args.sprite),
|
|
316
|
+
overwroteSharedData,
|
|
317
|
+
note,
|
|
318
|
+
});
|
|
319
|
+
},
|
|
320
|
+
});
|
|
321
|
+
|
|
322
|
+
gfx.command('render', {
|
|
323
|
+
description:
|
|
324
|
+
'Render a sprite (0-255) or the whole `sheet` to an UPSCALED, palette-accurate PNG a multimodal agent can LOOK at (the JUDGE surface). Always reports BOTH the PNG path AND the char grid. CTAs: render -> set -> render.',
|
|
325
|
+
args: z.object({
|
|
326
|
+
target: z
|
|
327
|
+
.string()
|
|
328
|
+
.describe(
|
|
329
|
+
'A sprite index 0-255 to render, or the literal `sheet` for the whole 128x128 spritesheet.',
|
|
330
|
+
),
|
|
331
|
+
cart: z
|
|
332
|
+
.string()
|
|
333
|
+
.default('main.p8')
|
|
334
|
+
.describe(
|
|
335
|
+
'The .p8 cart to read. Defaults to main.p8 in the current folder.',
|
|
336
|
+
),
|
|
337
|
+
}),
|
|
338
|
+
options: z.object({
|
|
339
|
+
out: z
|
|
340
|
+
.string()
|
|
341
|
+
.optional()
|
|
342
|
+
.describe(
|
|
343
|
+
'Output PNG path. Defaults to <cart-stem>-sprite-<n>.png / <cart-stem>-sheet.png next to the cart.',
|
|
344
|
+
),
|
|
345
|
+
}),
|
|
346
|
+
output: z.object({
|
|
347
|
+
target: z
|
|
348
|
+
.string()
|
|
349
|
+
.describe(
|
|
350
|
+
'What was rendered: a sprite index (as a string) or `sheet`.',
|
|
351
|
+
),
|
|
352
|
+
png: z
|
|
353
|
+
.string()
|
|
354
|
+
.describe(
|
|
355
|
+
'The absolute path of the PNG written (the JUDGE surface: LOOK at it).',
|
|
356
|
+
),
|
|
357
|
+
width: z
|
|
358
|
+
.number()
|
|
359
|
+
.describe('The rendered PNG width in pixels (upscaled).'),
|
|
360
|
+
height: z
|
|
361
|
+
.number()
|
|
362
|
+
.describe('The rendered PNG height in pixels (upscaled).'),
|
|
363
|
+
grid: z
|
|
364
|
+
.string()
|
|
365
|
+
.optional()
|
|
366
|
+
.describe(
|
|
367
|
+
'The sprite char grid (. = transparent, 0-f = colours) so a non-multimodal agent can reason over it; absent for `sheet`.',
|
|
368
|
+
),
|
|
369
|
+
aliasesMap: z
|
|
370
|
+
.boolean()
|
|
371
|
+
.describe(
|
|
372
|
+
'True for a single sprite 128-255, which aliases the shared map region (see `gfx set`).',
|
|
373
|
+
),
|
|
374
|
+
}),
|
|
375
|
+
examples: [
|
|
376
|
+
{description: 'Render sprite 1 to a viewable PNG', args: {target: '1'}},
|
|
377
|
+
{description: 'Render the whole spritesheet', args: {target: 'sheet'}},
|
|
378
|
+
{
|
|
379
|
+
description: 'Render sprite 3 of a specific cart to a chosen path',
|
|
380
|
+
args: {target: '3', cart: 'game.p8'},
|
|
381
|
+
options: {out: 'sprite3.png'},
|
|
382
|
+
},
|
|
383
|
+
],
|
|
384
|
+
run({args, options, error, ok}) {
|
|
385
|
+
// Parse the target: the literal `sheet` or a sprite index 0-255. A bad
|
|
386
|
+
// target is a caller error and never opens the cart.
|
|
387
|
+
const isSheet = args.target === 'sheet';
|
|
388
|
+
let sprite = -1;
|
|
389
|
+
if (!isSheet) {
|
|
390
|
+
sprite = Number(args.target);
|
|
391
|
+
if (!Number.isInteger(sprite) || sprite < 0 || sprite > 255) {
|
|
392
|
+
return error({
|
|
393
|
+
code: 'invalid-target',
|
|
394
|
+
message: `render target must be \`sheet\` or a sprite index 0-255, got "${args.target}"`,
|
|
395
|
+
exitCode: 1,
|
|
396
|
+
});
|
|
397
|
+
}
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
const cartPath = resolveCart(args.cart);
|
|
401
|
+
const loaded = loadCart(cartPath);
|
|
402
|
+
if (!loaded.ok) {
|
|
403
|
+
return error({code: loaded.code, message: loaded.message, exitCode: 1});
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
const sheet = GfxSheet.fromBody(loaded.cart.getSection('gfx'));
|
|
407
|
+
|
|
408
|
+
// Render the palette-accurate, nearest-neighbour-upscaled PNG and write it
|
|
409
|
+
// to the known path. The encoder is shrinko-FREE (pure TS hex -> PNG).
|
|
410
|
+
const png = isSheet
|
|
411
|
+
? renderSheetPng(sheet)
|
|
412
|
+
: renderSpritePng(sheet, sprite);
|
|
413
|
+
const outPath = options.out
|
|
414
|
+
? resolveCart(options.out)
|
|
415
|
+
: defaultRenderPath(cartPath, isSheet ? 'sheet' : sprite);
|
|
416
|
+
writeFileSync(outPath, png);
|
|
417
|
+
|
|
418
|
+
// ALWAYS emit BOTH the PNG path and the grid (for a single sprite), so the
|
|
419
|
+
// picopilot-art skill can branch view-vs-imagine. A `sheet` render has no
|
|
420
|
+
// single 8x8 grid, so the grid is omitted there (use `gfx show <n>`).
|
|
421
|
+
const width = isSheet
|
|
422
|
+
? GFX_WIDTH * SHEET_RENDER_SCALE
|
|
423
|
+
: SPRITE_SIZE * SPRITE_RENDER_SCALE;
|
|
424
|
+
const height = width;
|
|
425
|
+
const grid = isSheet ? undefined : nibblesToGrid(sheet.getSprite(sprite));
|
|
426
|
+
const aliasesMap = isSheet ? false : spriteAliasesMap(sprite);
|
|
427
|
+
|
|
428
|
+
// The see-and-fix loop: render (look) -> set (fix) -> render (re-look). A
|
|
429
|
+
// single sprite CTAs to `gfx set` then back to `gfx render` at the SAME
|
|
430
|
+
// target; `sheet` CTAs to `gfx show`/`gfx set` a specific sprite.
|
|
431
|
+
const cta = isSheet
|
|
432
|
+
? {
|
|
433
|
+
description:
|
|
434
|
+
'To edit a specific sprite from the sheet, then re-render:',
|
|
435
|
+
commands: [
|
|
436
|
+
{
|
|
437
|
+
command: 'gfx show <n>',
|
|
438
|
+
description: 'Read sprite n as an editable char grid.',
|
|
439
|
+
},
|
|
440
|
+
{
|
|
441
|
+
command: 'gfx render sheet',
|
|
442
|
+
description: 'Re-render the sheet to re-look.',
|
|
443
|
+
},
|
|
444
|
+
],
|
|
445
|
+
}
|
|
446
|
+
: {
|
|
447
|
+
description:
|
|
448
|
+
'Look at the PNG, then fix via the grid and re-render:',
|
|
449
|
+
commands: [
|
|
450
|
+
{
|
|
451
|
+
command: `gfx set ${sprite}`,
|
|
452
|
+
description: 'Write an edited char grid back into __gfx__.',
|
|
453
|
+
},
|
|
454
|
+
{
|
|
455
|
+
command: `gfx render ${sprite}`,
|
|
456
|
+
description: 'Re-render to re-look after the fix.',
|
|
457
|
+
},
|
|
458
|
+
],
|
|
459
|
+
};
|
|
460
|
+
|
|
461
|
+
return ok(
|
|
462
|
+
{
|
|
463
|
+
target: isSheet ? 'sheet' : String(sprite),
|
|
464
|
+
png: outPath,
|
|
465
|
+
width,
|
|
466
|
+
height,
|
|
467
|
+
grid,
|
|
468
|
+
aliasesMap,
|
|
469
|
+
},
|
|
470
|
+
{cta},
|
|
471
|
+
);
|
|
472
|
+
},
|
|
473
|
+
});
|
|
474
|
+
|
|
475
|
+
// gfx export: the raw 128x128 spritesheet PNG round-trip for external tools
|
|
476
|
+
// (shrinko-backed; DISTINCT from `gfx render`'s upscaled JUDGE image).
|
|
477
|
+
gfx.command('export', {
|
|
478
|
+
description:
|
|
479
|
+
'Export the cart spritesheet as a raw 128x128 PNG for external tools (via shrinko). Requires shrinko.',
|
|
480
|
+
args: z.object({
|
|
481
|
+
cart: z
|
|
482
|
+
.string()
|
|
483
|
+
.default('main.p8')
|
|
484
|
+
.describe('The .p8 cart to export from. Defaults to main.p8.'),
|
|
485
|
+
}),
|
|
486
|
+
options: z.object({
|
|
487
|
+
out: z
|
|
488
|
+
.string()
|
|
489
|
+
.optional()
|
|
490
|
+
.describe(
|
|
491
|
+
'Output PNG path. Defaults to <name>-sheet.png next to the cart.',
|
|
492
|
+
),
|
|
493
|
+
force: z
|
|
494
|
+
.boolean()
|
|
495
|
+
.default(false)
|
|
496
|
+
.describe(
|
|
497
|
+
'Overwrite an existing output PNG. Default off (no-clobber).',
|
|
498
|
+
),
|
|
499
|
+
}),
|
|
500
|
+
env: z.object({
|
|
501
|
+
PATH: z.string().optional().describe('Used to locate shrinko8.'),
|
|
502
|
+
}),
|
|
503
|
+
output: z.object({
|
|
504
|
+
outPath: z
|
|
505
|
+
.string()
|
|
506
|
+
.describe('Absolute path of the spritesheet PNG written.'),
|
|
507
|
+
}),
|
|
508
|
+
examples: [{description: 'Export main.p8 spritesheet to main-sheet.png'}],
|
|
509
|
+
async run({args, options, env, error, ok}) {
|
|
510
|
+
const cartPath = isAbsolute(args.cart)
|
|
511
|
+
? args.cart
|
|
512
|
+
: resolve(process.cwd(), args.cart);
|
|
513
|
+
if (!existsSync(cartPath)) {
|
|
514
|
+
return error({
|
|
515
|
+
code: 'cart-not-found',
|
|
516
|
+
message: `no cart at ${cartPath}`,
|
|
517
|
+
exitCode: 1,
|
|
518
|
+
});
|
|
519
|
+
}
|
|
520
|
+
const outPath =
|
|
521
|
+
options.out !== undefined
|
|
522
|
+
? isAbsolute(options.out)
|
|
523
|
+
? options.out
|
|
524
|
+
: resolve(process.cwd(), options.out)
|
|
525
|
+
: join(
|
|
526
|
+
dirname(cartPath),
|
|
527
|
+
`${basename(cartPath, extname(cartPath))}-sheet.png`,
|
|
528
|
+
);
|
|
529
|
+
if (existsSync(outPath) && !options.force) {
|
|
530
|
+
return error({
|
|
531
|
+
code: 'output-exists',
|
|
532
|
+
message: `refusing to overwrite existing ${outPath}; pass --force to overwrite`,
|
|
533
|
+
exitCode: 1,
|
|
534
|
+
});
|
|
535
|
+
}
|
|
536
|
+
const result = await shrinkoFactory(
|
|
537
|
+
env as NodeJS.ProcessEnv,
|
|
538
|
+
).exportSpritesheet(cartPath, outPath);
|
|
539
|
+
if (!result.ok) {
|
|
540
|
+
return error({
|
|
541
|
+
code: result.reason,
|
|
542
|
+
message: `shrinko is not installed. ${result.remedy} (needs: ${result.needs.join(', ')})`,
|
|
543
|
+
exitCode: 1,
|
|
544
|
+
});
|
|
545
|
+
}
|
|
546
|
+
return ok({outPath});
|
|
547
|
+
},
|
|
548
|
+
});
|
|
549
|
+
|
|
550
|
+
// gfx import: merge an external spritesheet PNG back into the cart's __gfx__.
|
|
551
|
+
// MERGE (not replace): code/map/sfx/music are preserved; only gfx changes.
|
|
552
|
+
gfx.command('import', {
|
|
553
|
+
description:
|
|
554
|
+
"Import a 128x128 spritesheet PNG into the cart's __gfx__ (merge; via shrinko). Requires shrinko.",
|
|
555
|
+
args: z.object({
|
|
556
|
+
png: z.string().describe('The spritesheet PNG to import.'),
|
|
557
|
+
cart: z
|
|
558
|
+
.string()
|
|
559
|
+
.default('main.p8')
|
|
560
|
+
.describe('The .p8 cart to import INTO. Defaults to main.p8.'),
|
|
561
|
+
}),
|
|
562
|
+
options: z.object({
|
|
563
|
+
out: z
|
|
564
|
+
.string()
|
|
565
|
+
.optional()
|
|
566
|
+
.describe(
|
|
567
|
+
'Output cart. Defaults to writing back into the cart in place (like `gfx set`).',
|
|
568
|
+
),
|
|
569
|
+
}),
|
|
570
|
+
env: z.object({
|
|
571
|
+
PATH: z.string().optional().describe('Used to locate shrinko8.'),
|
|
572
|
+
}),
|
|
573
|
+
output: z.object({
|
|
574
|
+
outPath: z
|
|
575
|
+
.string()
|
|
576
|
+
.describe('The cart written (the source cart when in place).'),
|
|
577
|
+
}),
|
|
578
|
+
examples: [
|
|
579
|
+
{description: 'Import sheet.png into main.p8', args: {png: 'sheet.png'}},
|
|
580
|
+
],
|
|
581
|
+
async run({args, options, env, error, ok}) {
|
|
582
|
+
const cartPath = isAbsolute(args.cart)
|
|
583
|
+
? args.cart
|
|
584
|
+
: resolve(process.cwd(), args.cart);
|
|
585
|
+
const pngPath = isAbsolute(args.png)
|
|
586
|
+
? args.png
|
|
587
|
+
: resolve(process.cwd(), args.png);
|
|
588
|
+
if (!existsSync(cartPath)) {
|
|
589
|
+
return error({
|
|
590
|
+
code: 'cart-not-found',
|
|
591
|
+
message: `no cart at ${cartPath}`,
|
|
592
|
+
exitCode: 1,
|
|
593
|
+
});
|
|
594
|
+
}
|
|
595
|
+
if (!existsSync(pngPath)) {
|
|
596
|
+
return error({
|
|
597
|
+
code: 'png-not-found',
|
|
598
|
+
message: `no PNG at ${pngPath}`,
|
|
599
|
+
exitCode: 1,
|
|
600
|
+
});
|
|
601
|
+
}
|
|
602
|
+
// Default: write back into the cart in place, matching `gfx set` (import IS
|
|
603
|
+
// a gfx edit that produces the readable cart, not a build artifact). `--out`
|
|
604
|
+
// writes a separate cart instead.
|
|
605
|
+
const outPath =
|
|
606
|
+
options.out !== undefined
|
|
607
|
+
? isAbsolute(options.out)
|
|
608
|
+
? options.out
|
|
609
|
+
: resolve(process.cwd(), options.out)
|
|
610
|
+
: cartPath;
|
|
611
|
+
const result = await shrinkoFactory(
|
|
612
|
+
env as NodeJS.ProcessEnv,
|
|
613
|
+
).importSpritesheet(cartPath, pngPath, outPath);
|
|
614
|
+
if (!result.ok) {
|
|
615
|
+
return error({
|
|
616
|
+
code: result.reason,
|
|
617
|
+
message: `shrinko is not installed. ${result.remedy} (needs: ${result.needs.join(', ')})`,
|
|
618
|
+
exitCode: 1,
|
|
619
|
+
});
|
|
620
|
+
}
|
|
621
|
+
return ok({outPath});
|
|
622
|
+
},
|
|
623
|
+
});
|
|
624
|
+
|
|
625
|
+
return gfx;
|
|
626
|
+
}
|
|
627
|
+
|
|
628
|
+
/**
|
|
629
|
+
* Registers the `gfx` command group (`gfx show` / `gfx set`) on the root CLI.
|
|
630
|
+
*
|
|
631
|
+
* `gfx` is a shrinko-FREE command group (ADR-0002): it reads/writes `__gfx__`
|
|
632
|
+
* entirely through the cart model + the TS char-grid codec, so it works with
|
|
633
|
+
* shrinko absent and never spawns a child process. `gfx set` carries the
|
|
634
|
+
* gfx/map overlap smart-refuse (ADR-0004): a write to a shared-bank sprite
|
|
635
|
+
* (128-255) whose current pixels hold data is refused unless authorised, so
|
|
636
|
+
* picopilot never SILENTLY destroys shared-region data. `gfx render` is likewise
|
|
637
|
+
* shrinko-free: a pure-TS hex->PNG encoder using PICO-8's fixed 16-colour palette
|
|
638
|
+
* (the eyes-loop stays dependency-light, ADR/US #7).
|
|
639
|
+
*/
|
|
640
|
+
export function registerGfx(
|
|
641
|
+
cli: Cli.Cli,
|
|
642
|
+
shrinkoFactory: ShrinkoAdapterFactory = defaultShrinkoFactory,
|
|
643
|
+
): void {
|
|
644
|
+
cli.command(buildGfxGroup(shrinkoFactory));
|
|
645
|
+
}
|