juicyjuicy 0.14.0 → 0.16.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/index.js +132 -29
- package/dist/skill/SKILL.md +23 -20
- package/dist/skill/reference/auto-pause.md +22 -0
- package/dist/skill/reference/gamepad.md +21 -0
- package/dist/skill/reference/languages.md +1 -1
- package/dist/skill/reference/manifest.md +2 -2
- package/dist/skill/reference/moderation.md +1 -1
- package/dist/skill/reference/mouse-lock.md +22 -0
- package/dist/skill/reference/multiplayer.md +20 -7
- package/dist/skill/reference/touch.md +44 -0
- package/dist/skill/reference/troubleshooting.md +1 -1
- package/dist/skill/reference/update-all-games.md +5 -3
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -19807,7 +19807,7 @@ function date4(params) {
|
|
|
19807
19807
|
}
|
|
19808
19808
|
|
|
19809
19809
|
// ../shared/src/cli.ts
|
|
19810
|
-
var CLI_RELEASE = "0.
|
|
19810
|
+
var CLI_RELEASE = "0.16.0";
|
|
19811
19811
|
function cliCommand(args) {
|
|
19812
19812
|
return `npx -y juicyjuicy@${CLI_RELEASE} ${args}`;
|
|
19813
19813
|
}
|
|
@@ -19924,7 +19924,7 @@ var LIMITS = {
|
|
|
19924
19924
|
var PUBLISH_LIMIT_WINDOW_HOURS = 24;
|
|
19925
19925
|
var RATE_LIMITS = {
|
|
19926
19926
|
/** New accounts, site-wide. */
|
|
19927
|
-
signUp: { limit:
|
|
19927
|
+
signUp: { limit: 60, windowSeconds: 60 * 60 },
|
|
19928
19928
|
/** New accounts per client IP (hashed). */
|
|
19929
19929
|
signUpNetwork: { limit: 3, windowSeconds: 24 * 60 * 60 },
|
|
19930
19930
|
/** Verification and password-reset mails per address (D88), keyed by a hash of it. */
|
|
@@ -20012,6 +20012,16 @@ var UPLOAD_BUDGET = {
|
|
|
20012
20012
|
/** Files one member may upload in the last 24 hours: 5 publishes at `LIMITS.maxFiles`. */
|
|
20013
20013
|
memberFilesPerDay: 15e3
|
|
20014
20014
|
};
|
|
20015
|
+
var PROVIDER_ALLOWANCES = {
|
|
20016
|
+
/** Neon Free: past it the compute is suspended until the next billing period. */
|
|
20017
|
+
neonComputeHoursPerMonth: 100,
|
|
20018
|
+
/** Neon Free: past it writes fail. */
|
|
20019
|
+
neonStorageBytes: 1024 * MB,
|
|
20020
|
+
/** Neon Free: past it the compute is suspended, as for CU-hours. */
|
|
20021
|
+
neonEgressBytesPerMonth: 5 * 1024 * MB,
|
|
20022
|
+
/** Cloudflare Email Service on Workers Paid; past it $0.35 per 1,000. */
|
|
20023
|
+
mailIncludedPerMonth: 3e3
|
|
20024
|
+
};
|
|
20015
20025
|
var THUMBNAIL_EXTENSIONS = ["png", "jpg", "jpeg", "webp"];
|
|
20016
20026
|
|
|
20017
20027
|
// ../shared/src/genres.ts
|
|
@@ -20055,6 +20065,21 @@ var GAME_GENRES = [
|
|
|
20055
20065
|
var GENRE_IDS = GAME_GENRES.map((genre) => genre.id);
|
|
20056
20066
|
var MAX_GENRES = 3;
|
|
20057
20067
|
var MAX_THEMES = 3;
|
|
20068
|
+
var MULTIPLAYER_TAG = "multiplayer";
|
|
20069
|
+
var MULTIPLAYER_WORDS = /* @__PURE__ */ new Set([
|
|
20070
|
+
"multiplayer",
|
|
20071
|
+
"multi-player",
|
|
20072
|
+
"online-multiplayer",
|
|
20073
|
+
"local-multiplayer",
|
|
20074
|
+
"co-op",
|
|
20075
|
+
"coop",
|
|
20076
|
+
"cooperative",
|
|
20077
|
+
"pvp",
|
|
20078
|
+
"2-player",
|
|
20079
|
+
"two-player",
|
|
20080
|
+
"2-players",
|
|
20081
|
+
"mmo"
|
|
20082
|
+
]);
|
|
20058
20083
|
var GENRE_LABELS = new Map(GAME_GENRES.map((genre) => [genre.id, genre.label]));
|
|
20059
20084
|
function isGenre(id) {
|
|
20060
20085
|
return GENRE_LABELS.has(id);
|
|
@@ -20179,9 +20204,7 @@ var THEME_GENRES = {
|
|
|
20179
20204
|
crossword: "word-trivia"
|
|
20180
20205
|
};
|
|
20181
20206
|
var DROPPED_TAGS = /* @__PURE__ */ new Set([
|
|
20182
|
-
"multiplayer",
|
|
20183
20207
|
"online",
|
|
20184
|
-
"online-multiplayer",
|
|
20185
20208
|
"mobile",
|
|
20186
20209
|
"mobile-friendly",
|
|
20187
20210
|
"touch",
|
|
@@ -20205,15 +20228,39 @@ var DROPPED_TAGS = /* @__PURE__ */ new Set([
|
|
|
20205
20228
|
"games",
|
|
20206
20229
|
"ai",
|
|
20207
20230
|
"ai-made",
|
|
20208
|
-
"ai-game"
|
|
20231
|
+
"ai-game",
|
|
20232
|
+
// Languages: the game's own field, with its own filter (`lang`), not a theme.
|
|
20233
|
+
"english",
|
|
20234
|
+
"polish",
|
|
20235
|
+
"polski",
|
|
20236
|
+
"german",
|
|
20237
|
+
"deutsch",
|
|
20238
|
+
"french",
|
|
20239
|
+
"francais",
|
|
20240
|
+
"spanish",
|
|
20241
|
+
"espanol",
|
|
20242
|
+
"italian",
|
|
20243
|
+
"italiano",
|
|
20244
|
+
"multilingual",
|
|
20245
|
+
"en",
|
|
20246
|
+
"pl",
|
|
20247
|
+
"de",
|
|
20248
|
+
"fr",
|
|
20249
|
+
"es",
|
|
20250
|
+
"it"
|
|
20209
20251
|
]);
|
|
20210
20252
|
function normalizeTags(tags) {
|
|
20211
20253
|
const themes = [];
|
|
20212
20254
|
const genres = /* @__PURE__ */ new Set();
|
|
20255
|
+
let multiplayer = false;
|
|
20213
20256
|
for (const raw of tags) {
|
|
20214
20257
|
const cleaned = raw.trim().toLowerCase().replace(/^#+/, "").replace(/[\s_]+/g, "-").replace(/[^a-z0-9-]/g, "").replace(/-+/g, "-").replace(/^-|-$/g, "");
|
|
20215
20258
|
if (cleaned.length < 2) continue;
|
|
20216
20259
|
const tag = TAG_SPELLINGS[cleaned] ?? cleaned;
|
|
20260
|
+
if (MULTIPLAYER_WORDS.has(tag)) {
|
|
20261
|
+
multiplayer = true;
|
|
20262
|
+
continue;
|
|
20263
|
+
}
|
|
20217
20264
|
if (DROPPED_TAGS.has(tag)) continue;
|
|
20218
20265
|
const genre = isGenre(tag) ? tag : GENRE_WORDS[tag];
|
|
20219
20266
|
if (genre) {
|
|
@@ -20225,7 +20272,7 @@ function normalizeTags(tags) {
|
|
|
20225
20272
|
if (!themes.includes(tag)) themes.push(tag);
|
|
20226
20273
|
}
|
|
20227
20274
|
return {
|
|
20228
|
-
tags: themes.slice(0, MAX_THEMES),
|
|
20275
|
+
tags: [...themes.slice(0, MAX_THEMES), ...multiplayer ? [MULTIPLAYER_TAG] : []],
|
|
20229
20276
|
genres: GENRE_IDS.filter((id) => genres.has(id)).slice(0, MAX_GENRES)
|
|
20230
20277
|
};
|
|
20231
20278
|
}
|
|
@@ -20737,37 +20784,42 @@ var GAME_FEATURES = [
|
|
|
20737
20784
|
},
|
|
20738
20785
|
{
|
|
20739
20786
|
id: "touch",
|
|
20740
|
-
name: "
|
|
20741
|
-
|
|
20742
|
-
|
|
20743
|
-
|
|
20744
|
-
|
|
20787
|
+
name: "Mobile optimization",
|
|
20788
|
+
summary: "The game plays well on phones and tablets: touch controls, screen fit, sound, speed.",
|
|
20789
|
+
adds: "Touch controls that fit the game, a layout that fits a phone screen in either orientation and survives rotating, menus that work by tap, and sound and speed that work on phones.",
|
|
20790
|
+
why: "Many players open links on their phone; without this they may only be able to watch, or get a game that's cut off, tiny or silent.",
|
|
20791
|
+
detection: { kind: "declared", input: "touch", addUntilDeclared: true },
|
|
20792
|
+
status: "available",
|
|
20793
|
+
recipe: "reference/touch.md",
|
|
20794
|
+
task: "optimize the game for mobile so it plays well on phones and tablets: touch controls, screen fit in both orientations, sound and speed",
|
|
20795
|
+
docs: "/docs/publish#touch"
|
|
20745
20796
|
},
|
|
20746
20797
|
{
|
|
20747
20798
|
id: "gamepad",
|
|
20748
20799
|
name: "Gamepad support",
|
|
20800
|
+
summary: "The game plays with a controller.",
|
|
20749
20801
|
adds: "The game plays with a controller through the browser's Gamepad API.",
|
|
20750
20802
|
why: "Some games just feel better on a controller, and couch players expect one to work.",
|
|
20751
|
-
detection: { kind: "declared" },
|
|
20752
|
-
status: "
|
|
20753
|
-
|
|
20754
|
-
|
|
20755
|
-
|
|
20756
|
-
name: "Mouse lock",
|
|
20757
|
-
adds: "Games that aim or steer with the mouse lock the cursor (Pointer Lock), so it can't leave the game.",
|
|
20758
|
-
why: "Without it, turning fast drags the cursor out of the frame or onto another screen, and the game stops responding.",
|
|
20759
|
-
detection: { kind: "declared" },
|
|
20760
|
-
status: "coming-soon"
|
|
20803
|
+
detection: { kind: "declared", input: "gamepad" },
|
|
20804
|
+
status: "available",
|
|
20805
|
+
recipe: "reference/gamepad.md",
|
|
20806
|
+
task: "add gamepad support so the game plays with a controller",
|
|
20807
|
+
docs: "/docs/publish#gamepad"
|
|
20761
20808
|
},
|
|
20762
20809
|
{
|
|
20763
20810
|
id: "auto-pause",
|
|
20764
20811
|
name: "Auto-pause",
|
|
20812
|
+
summary: "The game pauses when its tab is hidden or the window loses focus.",
|
|
20765
20813
|
adds: "The game pauses when its tab is hidden or the window loses focus.",
|
|
20766
20814
|
why: "Players who switch tabs for a moment don't come back to a lost game.",
|
|
20767
20815
|
detection: { kind: "declared" },
|
|
20768
|
-
status: "
|
|
20816
|
+
status: "available",
|
|
20817
|
+
recipe: "reference/auto-pause.md",
|
|
20818
|
+
task: "make the game pause by itself when its tab is hidden or the window loses focus",
|
|
20819
|
+
docs: "/docs/publish#auto-pause"
|
|
20769
20820
|
}
|
|
20770
20821
|
];
|
|
20822
|
+
var RETIRED_FEATURES = ["mouse-lock"];
|
|
20771
20823
|
function gameFeature(id) {
|
|
20772
20824
|
return GAME_FEATURES.find((feature) => feature.id === id);
|
|
20773
20825
|
}
|
|
@@ -20796,9 +20848,14 @@ function featureState(feature, input2) {
|
|
|
20796
20848
|
canAdd: languages.some((language) => !language.has)
|
|
20797
20849
|
};
|
|
20798
20850
|
}
|
|
20799
|
-
case "declared":
|
|
20851
|
+
case "declared": {
|
|
20852
|
+
const { input: inputKind, addUntilDeclared } = feature.detection;
|
|
20853
|
+
if (inputKind && input2.input?.includes(inputKind)) {
|
|
20854
|
+
return { feature, status: "on", canAdd: Boolean(addUntilDeclared) && !declared };
|
|
20855
|
+
}
|
|
20800
20856
|
if (input2.declared === null) return { feature, status: "unknown", canAdd: false };
|
|
20801
20857
|
return { feature, status: declared ? "on" : "off", canAdd: !declared };
|
|
20858
|
+
}
|
|
20802
20859
|
case "marker": {
|
|
20803
20860
|
if (input2.detected === null) return { feature, status: "unknown", canAdd: false };
|
|
20804
20861
|
const status = input2.detected.includes(feature.id) ? "on" : declared && input2.complete ? "missing" : declared ? "on" : "off";
|
|
@@ -20809,6 +20866,7 @@ function featureState(feature, input2) {
|
|
|
20809
20866
|
function featureFindings(input2) {
|
|
20810
20867
|
const findings = [];
|
|
20811
20868
|
for (const id of input2.declared) {
|
|
20869
|
+
if (RETIRED_FEATURES.includes(id)) continue;
|
|
20812
20870
|
const feature = gameFeature(id);
|
|
20813
20871
|
if (!feature) {
|
|
20814
20872
|
findings.push({
|
|
@@ -23934,6 +23992,12 @@ function titleLetters(title) {
|
|
|
23934
23992
|
var ERROR_STATUS = {
|
|
23935
23993
|
UNAUTHORIZED: 401,
|
|
23936
23994
|
FORBIDDEN: 403,
|
|
23995
|
+
/**
|
|
23996
|
+
* The token is publish-only (D104): it can start and finalize versions and list games and
|
|
23997
|
+
* versions, nothing else. Use `juicyjuicy login`, or a full token from /settings/tokens. CLIs up
|
|
23998
|
+
* to 0.14.0 get FORBIDDEN with the same message; 0.15.0 knows it (`NEWER_ERROR_CODES`).
|
|
23999
|
+
*/
|
|
24000
|
+
INSUFFICIENT_SCOPE: 403,
|
|
23937
24001
|
NOT_FOUND: 404,
|
|
23938
24002
|
VALIDATION_FAILED: 400,
|
|
23939
24003
|
INVALID_MANIFEST: 400,
|
|
@@ -27728,7 +27792,7 @@ var program = new Command();
|
|
|
27728
27792
|
// package.json
|
|
27729
27793
|
var package_default = {
|
|
27730
27794
|
name: "juicyjuicy",
|
|
27731
|
-
version: "0.
|
|
27795
|
+
version: "0.16.0",
|
|
27732
27796
|
description: "Publish AI-made browser games to juicyjuicy.games",
|
|
27733
27797
|
type: "module",
|
|
27734
27798
|
license: "MIT",
|
|
@@ -27818,6 +27882,14 @@ The sign-in page says why and how to contest it.`;
|
|
|
27818
27882
|
It won't be shown. Upload a different image to replace it.`;
|
|
27819
27883
|
case "profile_moderated":
|
|
27820
27884
|
return `JuicyJuicy's moderators removed ${note.subject ?? "something from your profile"}:${quoted(note.comment)}`;
|
|
27885
|
+
// Only the site's banner shows these (D108); a publish never sends them.
|
|
27886
|
+
case "version_held":
|
|
27887
|
+
case "report_held":
|
|
27888
|
+
case "image_held":
|
|
27889
|
+
case "score_removed":
|
|
27890
|
+
case "account_changed":
|
|
27891
|
+
case "game_edited":
|
|
27892
|
+
return `A notice about ${game} is waiting on the JuicyJuicy site:${quoted(note.comment ?? "see the banner there")}`;
|
|
27821
27893
|
}
|
|
27822
27894
|
}
|
|
27823
27895
|
function noteMessages(moderation, warnings, slug) {
|
|
@@ -28819,7 +28891,12 @@ ${i2}
|
|
|
28819
28891
|
|
|
28820
28892
|
// src/output.ts
|
|
28821
28893
|
var EXIT = { ok: 0, validation: 1, auth: 2, network: 3 };
|
|
28822
|
-
var AUTH_CODES = /* @__PURE__ */ new Set([
|
|
28894
|
+
var AUTH_CODES = /* @__PURE__ */ new Set([
|
|
28895
|
+
"UNAUTHORIZED",
|
|
28896
|
+
"EXPIRED_TOKEN",
|
|
28897
|
+
"ACCESS_DENIED",
|
|
28898
|
+
"INSUFFICIENT_SCOPE"
|
|
28899
|
+
]);
|
|
28823
28900
|
var NETWORK_CODES = /* @__PURE__ */ new Set([
|
|
28824
28901
|
"NETWORK_ERROR",
|
|
28825
28902
|
"NETWORK_BLOCKED",
|
|
@@ -28969,7 +29046,7 @@ var ApiClient = class {
|
|
|
28969
29046
|
if (isApiErrorBody(data)) {
|
|
28970
29047
|
const { code, message, details } = data.error;
|
|
28971
29048
|
const text3 = code === "PUBLISH_LIMIT_REACHED" && isPublishLimitDetails(details) ? publishLimitMessage(details) : code === "REVIEW_ALLOWANCE_REACHED" && isReviewAllowanceDetails(details) ? reviewAllowanceMessage(details) : message;
|
|
28972
|
-
throw new CliError(code, text3, { details, hint: moderationHint(code) });
|
|
29049
|
+
throw new CliError(code, text3, { details, hint: moderationHint(code) ?? scopeHint(code) });
|
|
28973
29050
|
}
|
|
28974
29051
|
if (!response.ok) {
|
|
28975
29052
|
if (looksBlocked(response.status, text2)) {
|
|
@@ -28980,6 +29057,9 @@ var ApiClient = class {
|
|
|
28980
29057
|
return data;
|
|
28981
29058
|
}
|
|
28982
29059
|
};
|
|
29060
|
+
function scopeHint(code) {
|
|
29061
|
+
return code === "INSUFFICIENT_SCOPE" ? "Publish-only tokens are for CI. For this command, unset JUICY_TOKEN and run `juicyjuicy login`, or use a full token from https://juicyjuicy.games/settings/tokens." : void 0;
|
|
29062
|
+
}
|
|
28983
29063
|
function networkError(baseUrl, error62) {
|
|
28984
29064
|
const cause = describeCause(error62);
|
|
28985
29065
|
if (!TLS_ERRORS.test(cause) && isBlockedCause(baseUrl, cause)) {
|
|
@@ -29468,7 +29548,7 @@ async function checkProject(dir, options = {}) {
|
|
|
29468
29548
|
const themes = normalizeTags(manifest.tags).tags;
|
|
29469
29549
|
if (themes.join() !== manifest.tags.join()) {
|
|
29470
29550
|
check3.warnings.push(
|
|
29471
|
-
`JuicyJuicy keeps tags as themes, the setting or subject (D103): these tags will be saved as [${themes.join(", ")}]. Genre words belong in "genres",
|
|
29551
|
+
`JuicyJuicy keeps tags as themes, the setting or subject (D103): these tags will be saved as [${themes.join(", ")}]. Genre words belong in "genres"; "multiplayer" is kept for a game players play together, other features show by themselves.`
|
|
29472
29552
|
);
|
|
29473
29553
|
}
|
|
29474
29554
|
const letters2 = titleNotEnglish(manifest.title) ? null : titleLetters(manifest.title);
|
|
@@ -30590,6 +30670,9 @@ async function publish(out, dir, options) {
|
|
|
30590
30670
|
if (started.keptDashboardThumbnail) {
|
|
30591
30671
|
out.info("Kept the thumbnail set on the game's dashboard; the screenshot wasn't used.");
|
|
30592
30672
|
}
|
|
30673
|
+
for (const warning of serverOnlyWarnings(checked.findings, finalized.findings)) {
|
|
30674
|
+
out.warn(warning);
|
|
30675
|
+
}
|
|
30593
30676
|
const done = started.keptDashboardThumbnail ? { ...finalized, keptDashboardThumbnail: true } : thumbnail?.auto ? { ...finalized, thumbnailAuto: true } : finalized;
|
|
30594
30677
|
let moderation = done.moderation;
|
|
30595
30678
|
const report = (data2, message) => {
|
|
@@ -30654,6 +30737,13 @@ async function publish(out, dir, options) {
|
|
|
30654
30737
|
);
|
|
30655
30738
|
report(data, `${manifest.title} v${done.version} is uploaded: ${done.gameUrl}`);
|
|
30656
30739
|
}
|
|
30740
|
+
function serverOnlyWarnings(local, server) {
|
|
30741
|
+
const key = (finding) => `${finding.rule}
|
|
30742
|
+
${finding.path ?? ""}
|
|
30743
|
+
${finding.message}`;
|
|
30744
|
+
const printed = new Set(local.map(key));
|
|
30745
|
+
return (server ?? []).filter((finding) => finding.severity === "warning" && !printed.has(key(finding))).map((finding) => `${finding.path ? `${finding.path}: ` : ""}${finding.message}`);
|
|
30746
|
+
}
|
|
30657
30747
|
async function waitForReview(api, slug, version2, {
|
|
30658
30748
|
timeoutMs = 45e3,
|
|
30659
30749
|
intervalMs = 2e3,
|
|
@@ -30749,12 +30839,25 @@ ${note}`)
|
|
|
30749
30839
|
games.length === 0 ? mine && !filtered ? "No games yet. Publish one with `npx juicyjuicy publish`." : "No games match." : [
|
|
30750
30840
|
heading,
|
|
30751
30841
|
...games.map(
|
|
30752
|
-
(game) => ` ${game.slug.padEnd(24)} ${(game.version ? `v${game.version}` : "-").padEnd(5)} ${(options.all ? `@${game.owner}` : game.status).padEnd(9)} ${game.gameUrl}`
|
|
30842
|
+
(game) => ` ${game.slug.padEnd(24)} ${(game.version ? `v${game.version}` : "-").padEnd(5)} ${(options.all ? `@${game.owner}` : game.status).padEnd(9)} ${game.gameUrl}${needsUpdateNote(game)}`
|
|
30753
30843
|
),
|
|
30754
|
-
...nextPage ? [`More: add --page ${nextPage}.`] : []
|
|
30844
|
+
...nextPage ? [`More: add --page ${nextPage}.`] : [],
|
|
30845
|
+
...games.some((game) => game.needsUpdate?.length) ? [
|
|
30846
|
+
`Some need a required update: ${cliCommand("check-games")} settles what it can and says what each still needs.`
|
|
30847
|
+
] : []
|
|
30755
30848
|
].join("\n")
|
|
30756
30849
|
);
|
|
30757
30850
|
}
|
|
30851
|
+
function needsUpdateNote(game) {
|
|
30852
|
+
const needs = game.needsUpdate ?? [];
|
|
30853
|
+
if (needs.length === 0) return "";
|
|
30854
|
+
const titles = needs.map((need) => need.title).join(", ");
|
|
30855
|
+
if (game.status === "published" && needs.some((need) => need.phase === "enforced")) {
|
|
30856
|
+
return ` needs update (${titles}): out of the public lists until updated`;
|
|
30857
|
+
}
|
|
30858
|
+
const deadline = needs.filter((need) => need.phase === "grace").map((need) => need.deadline).sort()[0];
|
|
30859
|
+
return ` needs update (${titles})${deadline ? ` by ${deadline.slice(0, 10)}` : ""}`;
|
|
30860
|
+
}
|
|
30758
30861
|
function versionLine(v) {
|
|
30759
30862
|
const state = v.live ? "live" : v.rejected ? "rejected" : v.changesRequested ? "changes" : v.inReview ? "held" : v.draft && v.status === "ready" ? "draft" : v.status;
|
|
30760
30863
|
const why = v.rejected ? " (rejected by an admin; it can never go live)" : v.changesRequested ? ` (an admin asked for changes${v.changesRequested.resolvedAt ? "; a later version settled them" : ""}; it can't go live)` : v.inReview ? ` (waiting for an admin's review: ${v.heldReason ?? "it needs a person to look at it"})` : "";
|
package/dist/skill/SKILL.md
CHANGED
|
@@ -9,22 +9,22 @@ JuicyJuicy hosts **static browser games** (HTML/JS/CSS/assets, WebAssembly) and
|
|
|
9
9
|
|
|
10
10
|
Run every CLI command with `--json`. Each output line is a JSON object; the **last** line is the result, either `{"ok": true, …}` or `{"ok": false, "error": {"code", "message", "details"?, "hint"?}}`. The exit code is `0` (ok), `1` (fix the game or `juicy.json`), `2` (log in) or `3` (network or server problem, so retry).
|
|
11
11
|
|
|
12
|
-
Run the CLI exactly as written here, `npx -y juicyjuicy@0.
|
|
12
|
+
Run the CLI exactly as written here, `npx -y juicyjuicy@0.16.0 …`: this skill is made for that release. If JuicyJuicy answers that the CLI is out of date, run the command its message gives. It updates the CLI and this skill together; read this skill again afterwards and use the release it names from then on.
|
|
13
13
|
|
|
14
14
|
## Procedure
|
|
15
15
|
|
|
16
16
|
### 1. Make sure the CLI is logged in
|
|
17
17
|
|
|
18
18
|
```bash
|
|
19
|
-
npx -y juicyjuicy@0.
|
|
19
|
+
npx -y juicyjuicy@0.16.0 whoami --json
|
|
20
20
|
```
|
|
21
21
|
|
|
22
22
|
If it exits with `2`, log in. `login` opens the user's browser and **waits up to 10 minutes** for them to approve:
|
|
23
23
|
|
|
24
|
-
- Run `npx -y juicyjuicy@0.
|
|
24
|
+
- Run `npx -y juicyjuicy@0.16.0 login --json` in the background if your shell can, and read its first output line (`{"event": "device_code", "userCode", "verificationUrl"}`) right away, and show the user the code and link without waiting for them to ask: the page shows the same code, and that's how they know the login is theirs.
|
|
25
25
|
- Then tell the user, with the real code and link: *"A JuicyJuicy page opened in your browser. Check it shows the code **WXTZ-BCDF** and click **Approve**. If no page opened, go to <verificationUrl>."*
|
|
26
26
|
- If your shell can't run commands in the background, say that first (without a code: it doesn't exist yet), then run it in the foreground with a timeout of at least 10 minutes. The code is also printed as `Your code: …`.
|
|
27
|
-
- The user needs a JuicyJuicy account
|
|
27
|
+
- The user needs a JuicyJuicy account. If they don't have one yet, they can make one on the sign-in page that opens.
|
|
28
28
|
|
|
29
29
|
In CI there's no browser: the `JUICY_TOKEN` environment variable holds a token instead.
|
|
30
30
|
|
|
@@ -45,7 +45,7 @@ Files and folders starting with `.` (`.env`, `.git/`, …) and `node_modules/` a
|
|
|
45
45
|
`juicy.json` sits in the project folder and describes the game. Create or update it with `init`, filling in what you know from the game you built:
|
|
46
46
|
|
|
47
47
|
```bash
|
|
48
|
-
npx -y juicyjuicy@0.
|
|
48
|
+
npx -y juicyjuicy@0.16.0 init --yes --json \
|
|
49
49
|
--title "Snake Deluxe" \
|
|
50
50
|
--description "Eat the apples, don't bite your tail. Arrow keys or swipe." \
|
|
51
51
|
--made-with "<your model name>, <the tool you run in>" \
|
|
@@ -57,7 +57,7 @@ npx -y juicyjuicy@0.14.0 init --yes --json \
|
|
|
57
57
|
- The slug (the `/g/<slug>` URL name) defaults to one made from the title. Once published it belongs to this user; reuse it to publish updates. **Never change the slug of a published game**, not even when the user renames it: change `title` only. A new slug is a new game. After the first publish, `juicy.json` also has an `id` that keeps the folder tied to its game; leave it as it is.
|
|
58
58
|
- `--languages`: the languages a player can switch between, English first, e.g. `en,pl`. Leave it out for a game that's only in English (the default is `en`).
|
|
59
59
|
- `--genres`: 1 to 3 from the list below, usually 1 or 2, the main one first. Required: `check` and `publish` stop without them, and refuse a genre that isn't on the list.
|
|
60
|
-
- `--tags`: up to 3 **themes**, the setting or subject, e.g. `space`, `pirates`, `zombies`, `blackjack`. Not genres (`puzzle`, `roguelike` go in `--genres`) and not features (
|
|
60
|
+
- `--tags`: up to 3 **themes**, the setting or subject, e.g. `space`, `pirates`, `zombies`, `blackjack`. Not genres (`puzzle`, `roguelike` go in `--genres`) and not features (touch, remixable, leaderboards, languages: JuicyJuicy shows those by itself from the game). One exception: add `multiplayer` (not counted among the 3) when players play together, online or on one screen, and the game doesn't use JuicyJuicy's rooms; it puts the game in the multiplayer filter. None is fine.
|
|
61
61
|
- `--age-rating`: who the game is for, from what's in it. **You rate every game you publish**; the game page shows it as a label.
|
|
62
62
|
|
|
63
63
|
All fields: [reference/manifest.md](reference/manifest.md).
|
|
@@ -127,7 +127,7 @@ To replace it, the user saves a screenshot as `thumbnail.png` in the project fol
|
|
|
127
127
|
|
|
128
128
|
Do this **every time**, also for updates. It protects the user and their players; JuicyJuicy's server runs its own checks as well, so this is help, not a formality to get past.
|
|
129
129
|
|
|
130
|
-
1. **Check.** Run `npx -y juicyjuicy@0.
|
|
130
|
+
1. **Check.** Run `npx -y juicyjuicy@0.16.0 check --json`. Errors (a failed result) must be fixed: explain each one (`details` has a `rule` and a `path`), and offer to fix it. If the user says no, **stop and don't publish**: the server would refuse it anyway. Warnings in the result (`findings`) go into step 2.
|
|
131
131
|
Every file in the result's `files` will be public. If `unusualFiles` isn't empty (names that look like keys or private data, project files like `package.json`, notes and other files a web game doesn't load), **show the user that list and ask whether those files should be public** before publishing; offer to move them out of the build folder or set `buildDir` to a folder with just the game.
|
|
132
132
|
2. **Security review.** Follow [reference/review.md](reference/review.md): keys and tokens, `innerHTML`/`eval` on network or multiplayer data, `http://` loads, trackers, login or payment forms, anything that leaves the page. Same rule: for what it marks as must-fix, fix it or don't publish.
|
|
133
133
|
3. **Standards.** Check the game against these and tell the user, in a sentence or two, what falls short; offer to fix it. A "no" still publishes: not every game can have touch controls.
|
|
@@ -142,7 +142,7 @@ Do this **every time**, also for updates. It protects the user and their players
|
|
|
142
142
|
- Saves progress with JuicyJuicy's window.juicy snippet, if it saves at all, so it follows signed-in players to any browser (not cookies: the play domain clears them).
|
|
143
143
|
- A Quit or Exit button, if it has one, sends JuicyJuicy's quit message instead of calling window.close().
|
|
144
144
|
|
|
145
|
-
The game runs in a frame on JuicyJuicy, inside a page. The frame can start at 0×0 or be hidden: when `innerWidth` or `innerHeight` is 0, pause and wait for a `resize` instead of restarting, and give every loop that searches (a free cell for food, a spawn point) a way out. A game stuck in a loop stops every other game in the player's tab from loading. Mouse events stop when the cursor leaves the frame: call `canvas.setPointerCapture(event.pointerId)` on `pointerdown` so a drag keeps getting `pointermove` and `pointerup` outside it. A game that aims or steers with the mouse (a shooter, a camera, a racer) calls `canvas.requestPointerLock()` on a click and reads `event.movementX`/`movementY`: the cursor hides and can't leave the game, not even onto a second screen in fullscreen, and Esc gives it back. Ask for the lock again on the next click.
|
|
145
|
+
The game runs in a frame on JuicyJuicy, inside a page. The frame can start at 0×0 or be hidden: when `innerWidth` or `innerHeight` is 0, pause and wait for a `resize` instead of restarting, and give every loop that searches (a free cell for food, a spawn point) a way out. A game stuck in a loop stops every other game in the player's tab from loading. Mouse events stop when the cursor leaves the frame: call `canvas.setPointerCapture(event.pointerId)` on `pointerdown` so a drag keeps getting `pointermove` and `pointerup` outside it. A game that aims or steers with the mouse (a shooter, a camera, a racer) calls `canvas.requestPointerLock()` on a click and reads `event.movementX`/`movementY`: the cursor hides and can't leave the game, not even onto a second screen in fullscreen, and Esc gives it back. Ask for the lock again on the next click. [reference/mouse-lock.md](reference/mouse-lock.md) has the whole recipe (pausing when the lock ends, menus, engines).
|
|
146
146
|
|
|
147
147
|
Esc belongs to the browser: it leaves fullscreen and Pointer Lock before the game sees it. `navigator.keyboard.lock()`, which hands Esc to a page in fullscreen, fails inside a frame, so don't rely on Esc alone: give pause or the menu another key too (like P).
|
|
148
148
|
|
|
@@ -156,7 +156,7 @@ Keep it short when there's nothing to say: "Checked: no problems found" is enoug
|
|
|
156
156
|
### 6. Publish
|
|
157
157
|
|
|
158
158
|
```bash
|
|
159
|
-
npx -y juicyjuicy@0.
|
|
159
|
+
npx -y juicyjuicy@0.16.0 publish --json --message "<what changed, in a few words>"
|
|
160
160
|
```
|
|
161
161
|
|
|
162
162
|
This makes the new version live: when the user says "publish", that's what they want. Don't add `--draft` (see "Other commands") unless they asked in words for it not to go live yet. Not for a first publish, a big game or one you're unsure of either: the user would have to find the dashboard and make it live themselves.
|
|
@@ -181,15 +181,18 @@ The game's page under My games on JuicyJuicy has an **Upgrades** panel; its Add
|
|
|
181
181
|
|
|
182
182
|
| Upgrade | Recipe | Declared in `juicy.json` as |
|
|
183
183
|
|---|---|---|
|
|
184
|
+
| Auto-pause | [reference/auto-pause.md](reference/auto-pause.md) | `"features": ["auto-pause"]` |
|
|
184
185
|
| Browser-friendly menu | [reference/browser-menu.md](reference/browser-menu.md) | `"features": ["browser-menu"]` |
|
|
185
186
|
| Cloud saves | [reference/cloud-saves.md](reference/cloud-saves.md) | `"features": ["cloud-saves"]` |
|
|
187
|
+
| Gamepad support | [reference/gamepad.md](reference/gamepad.md) | `"input": [… "gamepad"]` and `"features": ["gamepad"]` |
|
|
186
188
|
| Global highscores | [reference/highscores.md](reference/highscores.md) | `"features": ["highscores"]` |
|
|
189
|
+
| Mobile optimization | [reference/touch.md](reference/touch.md) | `"input": [… "touch"]` and `"features": ["touch"]` |
|
|
187
190
|
| Multi-language | [reference/languages.md](reference/languages.md) | `"languages": ["en", …]` |
|
|
188
191
|
| Multiplayer | [reference/multiplayer.md](reference/multiplayer.md) | `"features": ["multiplayer"]` |
|
|
189
192
|
|
|
190
|
-
|
|
193
|
+
An upgrade that doesn't fit the game (mouse lock for a game played by clicking, auto-pause for a turn-based one) is the recipe's to say: tell the user why, and change nothing unless they still want it.
|
|
191
194
|
|
|
192
|
-
1. **Find the project.** Log in first (step 1). The project is the folder whose `juicy.json` has the game's `slug`: the current folder, or one the user points you to. **If it isn't on this computer** (or you're in a chat app or sandbox), download the live version: `npx -y juicyjuicy@0.
|
|
195
|
+
1. **Find the project.** Log in first (step 1). The project is the folder whose `juicy.json` has the game's `slug`: the current folder, or one the user points you to. **If it isn't on this computer** (or you're in a chat app or sandbox), download the live version: `npx -y juicyjuicy@0.16.0 pull <slug> --json` writes it into a new folder named after the slug (`dir` in the result) with its `juicy.json`, and you work there. Read the result's `next`: the download is the built game, so for a bundled one (minified code), ask whether the user has the source somewhere, or use `sourceUrl`, before changing the bundle itself. `pull` only works for the user's own games (`NOT_FOUND` otherwise).
|
|
193
196
|
2. **Read the recipe** for the upgrade and make the change. Keep everything else about the game as it is.
|
|
194
197
|
3. **Build and try it** (step 2), then **declare it** in `juicy.json` as the table says, adding to what's already listed.
|
|
195
198
|
4. **Review and publish** as steps 5–7 say, with `--message "Add <upgrade>"`. In step 5's `check` result, `features.detected` lists what JuicyJuicy found in the files; a `feature-missing` warning means a declared upgrade isn't there yet: fix it before publishing.
|
|
@@ -197,14 +200,14 @@ Other features on the panel (touch controls, gamepad, mouse lock, auto-pause) ar
|
|
|
197
200
|
|
|
198
201
|
## Bring all my games up to date
|
|
199
202
|
|
|
200
|
-
When the user asks to bring their JuicyJuicy games up to date or fix a **Needs update** badge, when `check` warns `requirement-missing`, or after updating the CLI: follow [reference/update-all-games.md](reference/update-all-games.md). It starts with `npx -y juicyjuicy@0.
|
|
203
|
+
When the user asks to bring their JuicyJuicy games up to date or fix a **Needs update** badge, when `check` or `publish` warns `requirement-missing`, or after updating the CLI: follow [reference/update-all-games.md](reference/update-all-games.md). It starts with `npx -y juicyjuicy@0.16.0 check-games --json`, which checks every game of the user's at once and settles what a script can; the rest are code changes, one game at a time.
|
|
201
204
|
|
|
202
205
|
## Remix a game
|
|
203
206
|
|
|
204
207
|
A game marked remixable on JuicyJuicy can be downloaded, changed and published as a new game that links back to it. Log in first (step 1), then:
|
|
205
208
|
|
|
206
209
|
```bash
|
|
207
|
-
npx -y juicyjuicy@0.
|
|
210
|
+
npx -y juicyjuicy@0.16.0 remix <slug> --json
|
|
208
211
|
```
|
|
209
212
|
|
|
210
213
|
- The first line is `{"event": "license", "name", "text"}`: the remix license. Tell the user in a sentence what it says. In short: the remix may be published **on JuicyJuicy only**, never anywhere else, and the files aren't to be passed on. The full text is:
|
|
@@ -240,10 +243,10 @@ Everything else is in [reference/troubleshooting.md](reference/troubleshooting.m
|
|
|
240
243
|
|
|
241
244
|
## Other commands
|
|
242
245
|
|
|
243
|
-
- `npx -y juicyjuicy@0.
|
|
244
|
-
- `npx -y juicyjuicy@0.
|
|
245
|
-
- `npx -y juicyjuicy@0.
|
|
246
|
-
- `npx -y juicyjuicy@0.
|
|
247
|
-
- `npx -y juicyjuicy@0.
|
|
248
|
-
- `npx -y juicyjuicy@0.
|
|
249
|
-
- `npx -y juicyjuicy@0.
|
|
246
|
+
- `npx -y juicyjuicy@0.16.0 list --json`: the user's games; `list <slug>` shows one game's versions. `--all` lists every game on JuicyJuicy instead (60 a page, `--page 2` and on; `nextPage` in the result), `--owner <username>` another creator's. Narrow and order any list with `--search <title text>`, `--genre` (one of the genre ids above, or several: `board,cards-casino`), `--tag` (a theme), `--engine`, `--remixes`, `--multiplayer` (games with rooms), `--highscores` (games with a leaderboard), `--sort newest|upvoted|downvoted|played` and `--period 24h|week|month|all`, e.g. to find a game to remix.
|
|
247
|
+
- `npx -y juicyjuicy@0.16.0 publish --draft --json`: uploads a new version without making it live (players keep the current one). Only when the user asks for that in words (e.g. "upload it but don't put it live yet"), never on your own judgment. The result has `draft: true`; when they say so, make it live with `go-live`, or they can on the dashboard. Only the newest draft can go live.
|
|
248
|
+
- `npx -y juicyjuicy@0.16.0 go-live --slug <slug> --json`: makes the game's newest draft live, only when the user says so. Pass the draft's number (`go-live <version>`) when they named one, so a newer draft never goes live by surprise. A draft waiting for an admin's review can't go live yet (`FORBIDDEN`), and nor can one a moderator rejected (`VERSION_REJECTED`) or asked changes to (`CHANGES_REQUESTED`). There's no command to go back to an earlier version: when the user asks for one, explain that JuicyJuicy doesn't keep version history, and offer to fix the game or publish their own earlier files as a new version.
|
|
249
|
+
- `npx -y juicyjuicy@0.16.0 recheck <slug> --reply "<what changed>" --json`: asks JuicyJuicy's moderators for another look at a game they blocked, once the user fixed it (see [reference/moderation.md](reference/moderation.md)). Only with the user's OK.
|
|
250
|
+
- `npx -y juicyjuicy@0.16.0 pull <slug> --json`: downloads the live version of one of the user's own games into a new folder with its `juicy.json`, to change it when the project isn't on this computer (see "Add a JuicyJuicy upgrade").
|
|
251
|
+
- `npx -y juicyjuicy@0.16.0 check-games --json`: checks all the user's games against JuicyJuicy's features and required updates, declares the features it finds where a game never declared them, and lists what still needs a change (see "Bring all my games up to date").
|
|
252
|
+
- `npx -y juicyjuicy@0.16.0 open <slug>`: opens the game page in the browser.
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
# Upgrade: auto-pause
|
|
2
|
+
|
|
3
|
+
The game pauses by itself when its tab is hidden or the window loses focus. Read this when the user asks for auto-pause (the JuicyJuicy Upgrades panel's prompt names this file), then follow "Add a JuicyJuicy upgrade" in SKILL.md to publish it.
|
|
4
|
+
|
|
5
|
+
## Why
|
|
6
|
+
Players switch tabs, answer a message or click another window for a moment. A game that keeps running loses their run; one that only stops its loop because the browser slows hidden tabs comes back with a huge time step and jumps. Pausing on its own and waiting for them is what players expect.
|
|
7
|
+
|
|
8
|
+
**Only for games where time matters.** A game that waits for the player anyway (turn-based, puzzles without a timer, idle games that count time away on purpose) doesn't need it: say so to the user, and stop unless they want it.
|
|
9
|
+
|
|
10
|
+
## What to change
|
|
11
|
+
1. **Pause when the game can't be seen or played**: on `document`'s `visibilitychange` when `document.hidden` is true, and on `window`'s `blur` (the window, or the page around JuicyJuicy's frame, took the focus). Use the game's own pause, the same one its pause key or button opens, so the menu shows.
|
|
12
|
+
2. **Don't resume by itself.** When the tab comes back (`visibilitychange` with `document.hidden` false, or `focus`), leave the pause screen up; the player resumes with a click, a tap, the pause key or the Start button. Coming back into a running game is what this upgrade avoids.
|
|
13
|
+
3. **Only during play**: don't pause on the title screen, a menu, game over, or a multiplayer game other players depend on (there, keep it running and just mute, or follow the game's own rule).
|
|
14
|
+
4. **Stop what runs on its own**: the game's clock and timers count only playing time (pause the timer, or measure with the loop's capped `dt`), music and sounds pause or mute, and the loop's time step is capped (say 1/30 s) so a long frame after a pause can't jump the game.
|
|
15
|
+
5. **Save on hide** if the game saves: hiding is often the last chance before the tab closes (with cloud saves, call `juicy.save(...)` there).
|
|
16
|
+
Engines: Phaser pauses its loop on hidden tabs by itself but not on `blur`, and resumes by itself: add the game's pause scene on both, so the player resumes; Godot gets `NOTIFICATION_APPLICATION_FOCUS_OUT` and can set `get_tree().paused = true`; Unity has `OnApplicationFocus(false)` and `OnApplicationPause(true)`.
|
|
17
|
+
|
|
18
|
+
## Check it
|
|
19
|
+
Run the game, start playing, then switch tabs for ten seconds and come back: it's paused where it was, the clock didn't run, and it resumes when the player asks. Do the same by clicking outside the window. The title screen and menus don't pause.
|
|
20
|
+
|
|
21
|
+
## Declare it
|
|
22
|
+
Add `"auto-pause"` to `features` in `juicy.json` (create the list if it isn't there, adding to what's listed). JuicyJuicy can't detect this one from the files, so the declaration is what turns it On in the panel; only add it once the game pauses this way.
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
# Upgrade: gamepad support
|
|
2
|
+
|
|
3
|
+
The game plays with a controller. Read this when the user asks for gamepad support (the JuicyJuicy Upgrades panel's prompt names this file), then follow "Add a JuicyJuicy upgrade" in SKILL.md to publish it.
|
|
4
|
+
|
|
5
|
+
## Why
|
|
6
|
+
Some games feel better on a controller, and players with one plugged in expect it to work. Browsers support controllers through the Gamepad API, inside JuicyJuicy's frame too.
|
|
7
|
+
|
|
8
|
+
## What to change
|
|
9
|
+
1. **Read the pads every frame.** Browsers don't send events for buttons: in the game loop, call `navigator.getGamepads()` and read each connected pad's `buttons[i].pressed` and `axes`. Compare with the last frame to get "just pressed" for menus and jumps. A pad only shows up after the player presses a button on it (a browser rule), so don't wait for `gamepadconnected` to start reading.
|
|
10
|
+
2. **Use the standard layout** (`pad.mapping === "standard"`, what most pads report): button 0 is A/Cross (confirm, jump), 1 is B/Circle (back), 2 X/Square, 3 Y/Triangle, 4–5 the bumpers, 6–7 the triggers (`value` from 0 to 1), 8 Back/Select, 9 Start (pause and menu), 12–15 the d-pad up, down, left, right; axes 0–1 the left stick (x, y; down is +1), 2–3 the right stick.
|
|
11
|
+
3. **Dead zone**: treat a stick below about 0.2 from the centre as centred, so a worn stick doesn't drift. Let the d-pad and the left stick both move.
|
|
12
|
+
4. **Map pads onto the game's actions, not its keys.** If the game reads keys directly, add an input layer (`isDown("jump")` = Space or button 0) rather than faking keyboard events. Keep the keyboard working exactly as before.
|
|
13
|
+
5. **Menus work with a pad**: move the selection with the d-pad or stick, A confirms, B goes back, Start pauses. Show which button does what when a pad is in use ("A: jump"), keyboard hints otherwise.
|
|
14
|
+
6. **Local multiplayer** (if the game has it): one pad per player, by its `index`; a newly connected pad can join.
|
|
15
|
+
Engines: Phaser has `this.input.gamepad` (set `input: { gamepad: true }` in the game config); Godot maps joypad buttons in the Input Map (`JOY_BUTTON_A`…) and works in the web export; Unity's Input System supports gamepads in WebGL.
|
|
16
|
+
|
|
17
|
+
## Check it
|
|
18
|
+
Plug in a controller (or pair one), press a button, then play a full round with the pad only: menus, play, pause, game over, restart. Check the keyboard still works.
|
|
19
|
+
|
|
20
|
+
## Declare it
|
|
21
|
+
Add `"gamepad"` to `input` in `juicy.json` (`"input": ["keyboard", "gamepad"]`, keeping what's there), and `"gamepad"` to `features` (adding to the list). JuicyJuicy can't detect this one from the files, so the declaration is what turns it On in the panel; only add it once a full round plays with the pad.
|
|
@@ -16,4 +16,4 @@ JuicyJuicy offers English, French, Italian, German, Spanish and Polish (`en`, `f
|
|
|
16
16
|
Run the game, switch to each language and go through the menus and the first minute of play: no missing keys (an untranslated key shows up as the key's name), no clipped text, and the choice is still there after a reload.
|
|
17
17
|
|
|
18
18
|
## Declare it
|
|
19
|
-
Set `languages` in `juicy.json` to every language the game now has, English first, e.g. `"languages": ["en", "fr", "pl"]` (or `npx -y juicyjuicy@0.
|
|
19
|
+
Set `languages` in `juicy.json` to every language the game now has, English first, e.g. `"languages": ["en", "fr", "pl"]` (or `npx -y juicyjuicy@0.16.0 init --yes --json --languages en,fr,pl`). Don't add it to `features`: JuicyJuicy reads `languages`, and shows the game in each language's filter.
|
|
@@ -37,10 +37,10 @@ The manifest in the project folder. `juicyjuicy init` writes it; edit it by hand
|
|
|
37
37
|
| `engine` | | `vanilla`, `phaser`, `threejs`, `pixi`, `godot`, `unity` or `other`. | `--engine` |
|
|
38
38
|
| `madeWith` | | Up to 5 models/tools, e.g. `["Claude Opus 5.5", "Claude Code"]`. | `--made-with "a, b"` |
|
|
39
39
|
| `genres` | yes | 1 to 3 ids from the genre list in SKILL.md step 3, e.g. `["puzzle"]`, the main one first. `check` and `publish` stop without them, or with an id that isn't on the list. | `--genres a,b` |
|
|
40
|
-
| `tags` | | Themes: up to 3 lowercase words or `word-word` for the setting or subject, e.g. `space`, `pirates`, `blackjack`. Not genres or features (
|
|
40
|
+
| `tags` | | Themes: up to 3 lowercase words or `word-word` for the setting or subject, e.g. `space`, `pirates`, `blackjack`. Not genres or features (touch, remixable: JuicyJuicy shows those by itself), except `multiplayer`, not counted among the 3, for a game players play together without JuicyJuicy's rooms (its own netcode, or one screen). JuicyJuicy cleans them up when saving (spellings fixed, `co-op`, `pvp` and the like saved as `multiplayer`, genre and other feature words moved out, at most 3 themes kept; `check` warns with what it will save). | `--tags a,b` |
|
|
41
41
|
| `ageRating` | | Who the game is for: `everyone`, `12+` or `16+`, from its content (SKILL.md step 3 says how to pick). Rate every game you publish. Left out, a publish keeps the game's rating; a rating set by JuicyJuicy's moderators wins over this one, and `publish` writes it here. | `--age-rating` |
|
|
42
42
|
| `languages` | | Up to 10 language codes a player can switch between, English first, e.g. `["en", "pl"]`. Default `["en"]`. Shown as a language filter on JuicyJuicy. | `--languages a,b` |
|
|
43
|
-
| `features` | yes | The JuicyJuicy upgrades the game has, by id: `browser-menu`, `cloud-saves`; `[]` for none. Required: `check` and `publish` stop without it, and `init` adds `[]`. Add an id only once the game has the feature (see SKILL.md, "Add a JuicyJuicy upgrade"); `check` warns when one is listed but missing. Multi-language is `languages`, not a feature id. | |
|
|
43
|
+
| `features` | yes | The JuicyJuicy upgrades the game has, by id: `auto-pause`, `browser-menu`, `cloud-saves`, `gamepad`, `highscores`, `multiplayer`, `touch`; `[]` for none. Required: `check` and `publish` stop without it, and `init` adds `[]`. Add an id only once the game has the feature (see SKILL.md, "Add a JuicyJuicy upgrade"); `check` warns when one is listed but missing. Multi-language is `languages`, not a feature id; `touch` and `gamepad` go in `input` too. | |
|
|
44
44
|
| `input` | | Any of `keyboard`, `mouse`, `touch`, `gamepad`. | `--input a,b` |
|
|
45
45
|
| `orientation` | | `landscape`, `portrait` or `any` (default). | `--orientation` |
|
|
46
46
|
| `aspectRatio` | | `"W:H"` like `"16:9"` or `"4:3"`, or `"fill"` to fill the player. Default `16:9`. | `--aspect-ratio` |
|
|
@@ -25,7 +25,7 @@ JuicyJuicy's moderators (its admins) can leave the user a written comment about
|
|
|
25
25
|
- after a change request or a rejection: run the review steps and publish a new version (`publish --json`, with a `--message` that says what changed). Tell the user it goes through the safety check again and may wait for a moderator.
|
|
26
26
|
- after a block: publish if the game's files changed (a new title or description in `juicy.json` also goes out with a publish), then ask for another look:
|
|
27
27
|
```bash
|
|
28
|
-
npx -y juicyjuicy@0.
|
|
28
|
+
npx -y juicyjuicy@0.16.0 recheck <slug> --reply "<what the user changed, in their words>" --json
|
|
29
29
|
```
|
|
30
30
|
The game stays unlisted until an admin lists it again; tell the user that. Write the reply with the user, and keep it to what was changed.
|
|
31
31
|
4. **If the user doesn't want to change it**, stop. A version with changes requested or rejected never goes live, and a blocked game stays unlisted. Don't try to get around the note: don't republish the same thing to clear it, rename the game, or publish it under a new slug.
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
# Upgrade: mouse lock
|
|
2
|
+
|
|
3
|
+
Aiming or steering with the mouse keeps the cursor in the game. Read this when the publish standards in SKILL.md find a game that aims or steers with the mouse without locking it, or when the user asks for mouse lock. JuicyJuicy doesn't lock the mouse for a game: the frame only allows it, and the game has to ask. If the user's game needs it, follow "Add a JuicyJuicy upgrade" in SKILL.md to publish the change.
|
|
4
|
+
|
|
5
|
+
## Why
|
|
6
|
+
In a game that turns a camera or aims with the mouse, turning fast drags the cursor out of the game's frame, onto the page or another screen, and the game stops getting the movement. Pointer Lock hides the cursor and keeps sending movement however far the mouse goes. JuicyJuicy's frame allows it.
|
|
7
|
+
|
|
8
|
+
**Only for games that aim or steer with mouse movement** (first-person, twin-stick, flight, top-down aiming that turns the view). A game played by clicking things on screen (puzzles, cards, strategy) needs the cursor: tell the user mouse lock doesn't fit it, and stop.
|
|
9
|
+
|
|
10
|
+
## What to change
|
|
11
|
+
1. **Lock on a click in the game**, never on load: browsers only allow it after a user gesture. On the canvas's `click` (or a "Click to play" overlay), call `canvas.requestPointerLock()`. Where supported, `requestPointerLock({ unadjustedMovement: true })` gives raw movement; fall back to the plain call if it rejects.
|
|
12
|
+
2. **Read movement, not position**: while locked, use `event.movementX` and `movementY` from `mousemove` (the cursor position doesn't change). Keep the game's sensitivity setting, or add one.
|
|
13
|
+
3. **Follow the lock state**: on `document`'s `pointerlockchange`, `document.pointerLockElement === canvas` says whether it's locked. When the lock ends (the player pressed Esc, switched tabs, or the browser took it back), **pause** and show "Click to play"; clicking locks again. Handle `pointerlockerror` with the same screen.
|
|
14
|
+
4. **Menus unlock**: opening the pause menu or any screen with buttons calls `document.exitPointerLock()`, so the player can click them; going back to play locks again on the next click.
|
|
15
|
+
5. **Esc belongs to the browser**: in a locked or fullscreen game, Esc only gives the mouse back. Pause on the lock ending (step 3), not on the Esc key, and give pause a key other than Esc (like P), as [browser-menu.md](browser-menu.md) explains.
|
|
16
|
+
Engines: Phaser has `this.input.mouse.requestPointerLock()` and `this.input.mouse.locked`; Godot sets `Input.mouse_mode = Input.MOUSE_MODE_CAPTURED` from an input event and reads `InputEventMouseMotion.relative`; Unity sets `Cursor.lockState = CursorLockMode.Locked` after a click.
|
|
17
|
+
|
|
18
|
+
## Check it
|
|
19
|
+
Run the game: a click locks the cursor and turning works far past the frame's edges; Esc gives the mouse back and the game pauses with "Click to play"; a click resumes; menus can be clicked.
|
|
20
|
+
|
|
21
|
+
## Declare it
|
|
22
|
+
Nothing to declare: mouse lock isn't a row in the Upgrades panel, so leave `features` as it is. A `juicy.json` that already lists `mouse-lock` is fine as it is too.
|
|
@@ -95,9 +95,12 @@ window.juicy.rooms || (function (juicy) {
|
|
|
95
95
|
this.link = null;
|
|
96
96
|
this.opened = false;
|
|
97
97
|
this.closed = false;
|
|
98
|
+
this.reconnecting = false;
|
|
98
99
|
}
|
|
99
100
|
// Events: "message" (data, fromId), "join" (player), "leave" (player), "host" (id),
|
|
100
|
-
// "lock" (locked), "public" (isPublic), "error" (error), "close" (reason)
|
|
101
|
+
// "lock" (locked), "public" (isPublic), "error" (error), "close" (reason),
|
|
102
|
+
// "reconnecting" (attempt: 1 to 3; show "Reconnecting...", sends meanwhile are dropped) and
|
|
103
|
+
// "reconnected" (back in the room; "join", "leave" and "host" have told what changed).
|
|
101
104
|
Room.prototype.on = function (event, handler) {
|
|
102
105
|
(this.handlers[event] = this.handlers[event] || []).push(handler);
|
|
103
106
|
return this;
|
|
@@ -148,6 +151,7 @@ window.juicy.rooms || (function (juicy) {
|
|
|
148
151
|
Room.prototype.end = function (reason) {
|
|
149
152
|
if (this.closed) return;
|
|
150
153
|
this.closed = true;
|
|
154
|
+
this.reconnecting = false;
|
|
151
155
|
var link = this.link;
|
|
152
156
|
this.link = null;
|
|
153
157
|
if (link) link.close();
|
|
@@ -202,9 +206,10 @@ window.juicy.rooms || (function (juicy) {
|
|
|
202
206
|
}
|
|
203
207
|
};
|
|
204
208
|
|
|
205
|
-
// JuicyJuicy's relay: resolves once it has welcomed this player.
|
|
206
|
-
|
|
207
|
-
|
|
209
|
+
// JuicyJuicy's relay: resolves once it has welcomed this player. "ticket" is the player's answer
|
|
210
|
+
// with a ticket when there is one already, else a new one is asked for.
|
|
211
|
+
function connect(room, query, ticket) {
|
|
212
|
+
return (ticket ? Promise.resolve(ticket) : askTicket(true)).then(function (m) {
|
|
208
213
|
return new Promise(function (resolve, reject) {
|
|
209
214
|
var socket = new WebSocket(m.url + "?ticket=" + encodeURIComponent(m.ticket) + query);
|
|
210
215
|
var welcomed = false;
|
|
@@ -227,6 +232,10 @@ window.juicy.rooms || (function (juicy) {
|
|
|
227
232
|
welcomed = true;
|
|
228
233
|
room.link = link;
|
|
229
234
|
room.receive(message);
|
|
235
|
+
if (room.reconnecting) {
|
|
236
|
+
room.reconnecting = false;
|
|
237
|
+
room.emit("reconnected");
|
|
238
|
+
}
|
|
230
239
|
return resolve(room);
|
|
231
240
|
}
|
|
232
241
|
if (room.link === link) room.receive(message);
|
|
@@ -246,6 +255,8 @@ window.juicy.rooms || (function (juicy) {
|
|
|
246
255
|
function reconnect(room, attempt) {
|
|
247
256
|
if (room.closed) return;
|
|
248
257
|
if (attempt === 3) return room.end("DISCONNECTED");
|
|
258
|
+
room.reconnecting = true;
|
|
259
|
+
room.emit("reconnecting", attempt + 1);
|
|
249
260
|
setTimeout(function () {
|
|
250
261
|
if (room.closed) return;
|
|
251
262
|
connect(room, "&room=" + room.code).catch(function (e) {
|
|
@@ -447,7 +458,7 @@ window.juicy.rooms || (function (juicy) {
|
|
|
447
458
|
var room = new Room(null);
|
|
448
459
|
if (m) {
|
|
449
460
|
return connect(room, "&create=1&max=" + max + (listed ? "&public=1" : "") +
|
|
450
|
-
(label ? "&label=" + label : ""));
|
|
461
|
+
(label ? "&label=" + label : ""), m);
|
|
451
462
|
}
|
|
452
463
|
room.code = newCode();
|
|
453
464
|
return joinLocal(room, max, { public: listed, label: label });
|
|
@@ -458,7 +469,7 @@ window.juicy.rooms || (function (juicy) {
|
|
|
458
469
|
code = String(code || "").trim().toUpperCase();
|
|
459
470
|
return askTicket(true).then(function (m) {
|
|
460
471
|
var room = new Room(code);
|
|
461
|
-
return m ? connect(room, "&room=" + encodeURIComponent(code)) : joinLocal(room, 0);
|
|
472
|
+
return m ? connect(room, "&room=" + encodeURIComponent(code), m) : joinLocal(room, 0);
|
|
462
473
|
});
|
|
463
474
|
},
|
|
464
475
|
// The game's public rooms that can be joined now (not full or locked), newest first:
|
|
@@ -550,6 +561,8 @@ function start(room) {
|
|
|
550
561
|
room.on("host", () => showLobby(room)); // the new host gets the switch and Start
|
|
551
562
|
room.on("public", (isPublic) => showPublic(isPublic));
|
|
552
563
|
room.on("message", (data, from) => (data.k === "start" ? beginGame(room) : handle(data, from)));
|
|
564
|
+
room.on("reconnecting", () => showStatus("Reconnecting…")); // a dropped connection, up to 3 tries
|
|
565
|
+
room.on("reconnected", () => showStatus(""));
|
|
553
566
|
room.on("close", (reason) => { showScreen("multiplayer"); showError(reason); });
|
|
554
567
|
}
|
|
555
568
|
```
|
|
@@ -560,7 +573,7 @@ function start(room) {
|
|
|
560
573
|
- `juicy.rooms.list({ label })` resolves to the public rooms that can be joined now, newest first, at most 50: `[{ code, host, players, max, label }]` (`host` is the host's login). Full and locked rooms aren't in it. With `label`, only rooms with that label. It rejects like `create()` (`SIGNED_OUT`, `RATE_LIMITED`, `PAUSED`, `OFF`, `FAILED`, `TIMEOUT`). Join one with `join(room.code)`.
|
|
561
574
|
- The room: `room.code`, `room.me` (this player's id, like `"p2"`), `room.host` (the host's id), `room.isHost()`, `room.players` (`{ id, name }`, name being the member's JuicyJuicy login), `room.player(id)`, `room.locked`, `room.max`, `room.public` and `room.label`.
|
|
562
575
|
- `room.send(data)` sends any JSON value to everyone else; `room.send(data, id)` only to that player. A player never gets their own messages.
|
|
563
|
-
- `room.on(event, handler)`: `"message"` (data, fromId), `"join"` (player), `"leave"` (player), `"host"` (the new host's id), `"lock"` (locked), `"public"` (whether it's listed), `"error"` (an `Error` with a `code`: `RATE_LIMITED`, `TOO_LARGE`, `NOT_HOST`, `NO_PLAYER`, `INVALID`; the room goes on), `"close"` (reason: `LEFT`, `KICKED`, `REPLACED` (the same player joined from another tab), `ROOM_NOT_FOUND`, `TOO_FAST`, `GAME_GONE
|
|
576
|
+
- `room.on(event, handler)`: `"message"` (data, fromId), `"join"` (player), `"leave"` (player), `"host"` (the new host's id), `"lock"` (locked), `"public"` (whether it's listed), `"error"` (an `Error` with a `code`: `RATE_LIMITED`, `TOO_LARGE`, `NOT_HOST`, `NO_PLAYER`, `INVALID`; the room goes on), `"reconnecting"` (the attempt, 1 to 3: the connection dropped and the room is trying to come back; show "Reconnecting…" over the game, and know that `send()` does nothing meanwhile), `"reconnected"` (back in the room; `join`, `leave` and `host` have already told what changed while away), `"close"` (reason: `LEFT`, `KICKED`, `REPLACED` (the same player joined from another tab), `ROOM_NOT_FOUND`, `TOO_FAST`, `GAME_GONE` (the game was taken down: its open rooms close within 30 seconds), `OFF` or `DISCONNECTED` (the three tries failed)).
|
|
564
577
|
- The host only: `room.lock()` stops new players joining (players who were in can come back) and takes a public room off the list, `room.lock(false)` opens it again; `room.kick(id)` removes a player for good (they don't see the room in the list either); `room.setPublic(false)` makes a public room invite-only, `room.setPublic()` lists it.
|
|
565
578
|
- `room.invite()` shows JuicyJuicy's share sheet over the game and returns the link (`https://juicyjuicy.games/g/<slug>/play?room=CODE`).
|
|
566
579
|
- `room.leave()` leaves the room.
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
# Upgrade: mobile optimization
|
|
2
|
+
|
|
3
|
+
The game plays well on phones and tablets: controls, screen, sound and speed. Read this when the user asks for mobile optimization or touch controls (the JuicyJuicy Upgrades panel's prompt names this file), then follow "Add a JuicyJuicy upgrade" in SKILL.md to publish it.
|
|
4
|
+
|
|
5
|
+
## Why
|
|
6
|
+
Many players open a shared link on their phone. JuicyJuicy shows the game there in a frame that fills the screen below a slim bar. Players without a keyboard or mouse can only watch unless the game takes touches, and a game laid out for a desktop window ends up cut off or tiny. Games with touch also get the **Touch controls** chip and filter on JuicyJuicy.
|
|
7
|
+
|
|
8
|
+
## What to change
|
|
9
|
+
Go through every part, also when the game already takes touches: fix what falls short and leave what's already right. Keyboard and mouse players keep the game as it was.
|
|
10
|
+
|
|
11
|
+
### Controls
|
|
12
|
+
1. **Pick the controls that fit the game**, and say which to the user before building, in a sentence:
|
|
13
|
+
- **Taps and drags** for games already played with the mouse (clicks, aiming, dragging pieces): make the pointer code work for touch. Use Pointer Events (`pointerdown`, `pointermove`, `pointerup`) instead of mouse events, so one handler takes both; for multi-touch, track each `pointerId`.
|
|
14
|
+
- **Swipes** for games with four directions (snake, 2048, tile puzzles): a swipe longer than about 30 CSS pixels in its main direction is the arrow key.
|
|
15
|
+
- **On-screen buttons** for games with keys held down (platformers, shooters, racing): a d-pad or a virtual stick on the left, action buttons on the right. Each button is at least 44×44 CSS pixels with space between, semi-transparent over the game, and holding it acts like holding the key (press on `pointerdown`, release on `pointerup`, `pointercancel` and `pointerleave`). Moving and jumping at once must work: track each `pointerId`.
|
|
16
|
+
Engines: Phaser has `this.input.on("pointerdown", …)` and `this.input.addPointer()` for more fingers; Godot has `InputEventScreenTouch`/`ScreenDrag` and a `TouchScreenButton` node that triggers an input action; Unity uses its Input System's on-screen controls.
|
|
17
|
+
2. **Show on-screen buttons only on touch screens**: when `matchMedia("(pointer: coarse)").matches`, or from the first `touchstart`/`pointerdown` with `pointerType === "touch"`.
|
|
18
|
+
3. **Stop the browser taking the gestures.** On the game's canvas or container, set the CSS `touch-action: none` and `user-select: none; -webkit-user-select: none; -webkit-touch-callout: none`, so drags don't scroll or zoom the page and a long press doesn't select or open a menu. On `html, body` set `overscroll-behavior: none; overflow: hidden`, unless the game is a page meant to scroll. Keep `<meta name="viewport" content="width=device-width, initial-scale=1">`.
|
|
19
|
+
4. **Menus and dialogs work by tap**: every key-only action ("press Enter to start", "Space to restart") gets a tap target too, and the help text says "tap" on touch screens. Anything shown only on hover also needs a tap.
|
|
20
|
+
|
|
21
|
+
### Screen
|
|
22
|
+
5. **Fit the frame.** Size the game from the frame (`innerWidth` and `innerHeight`, or CSS `100%`): no fixed 1280×720 page with scrollbars, and nothing important cut off at a narrow width.
|
|
23
|
+
6. **Survive every resize.** Rotating the phone, the browser's address bar sliding away and the on-screen keyboard all resize the frame. Listen for `resize`, lay the game out again and keep playing: never restart the level or lose the score because of it.
|
|
24
|
+
7. **Stay readable.** Size text and buttons in CSS pixels from the frame, not from the game's internal resolution, so a 1920-wide canvas shrunk to a phone doesn't shrink its text with it: body text at least 14 CSS pixels, tap targets at least 44.
|
|
25
|
+
8. **Work both ways round if you can**, with a layout for tall and for wide frames, as the shapes are very different. If the game only works one way, set `orientation` in `juicy.json` (`landscape` or `portrait`) and, when the frame is the other way round, pause and show "Rotate your phone" over the game until a `resize` puts it right. Nothing rotates the game for you: `screen.orientation.lock()` doesn't work on iPhones.
|
|
26
|
+
9. **Keep controls clear.** On-screen controls sit at least 16 CSS pixels from the frame's edges, under the thumbs, and don't cover the player or the action. In a tall frame, put them below the play area when there's room.
|
|
27
|
+
10. **Never need fullscreen.** iPhone Safari can't put a game in fullscreen: hide the game's own fullscreen button when `document.fullscreenEnabled` is false.
|
|
28
|
+
|
|
29
|
+
### Phone browsers
|
|
30
|
+
11. **Sound starts on a tap.** Phones keep audio silent until the player touches the game: create or `resume()` the `AudioContext` in a `pointerup` or `click` handler (iPhones don't accept `pointerdown`), and `resume()` again when the page becomes visible, since a call or switching apps suspends it.
|
|
31
|
+
12. **Nothing depends on the mouse or keyboard being there.** Phones have no Pointer Lock: when `requestPointerLock` is missing or fails, aim by dragging or with a stick. Only a real `<input>` brings up the on-screen keyboard: focus it inside the tap's handler and give it `font-size: 16px`, or iPhones zoom the page.
|
|
32
|
+
13. **Run smoothly.** Cap the drawing resolution on touch screens at `Math.min(devicePixelRatio, 2)`, and use fewer particles or effects there if a round drops frames. Desktop keeps its full quality.
|
|
33
|
+
14. **Stop when hidden.** Phones switch apps often: on `visibilitychange` to hidden, pause a real-time game and stop its sound.
|
|
34
|
+
|
|
35
|
+
## Check it
|
|
36
|
+
Run the game in the browser's device mode (a phone size, touch on), upright and on its side (about 390×844 and 844×390), and play a full round with touch only: start, play, pause, game over, restart. Rotate in the middle of a round and check it carries on. Check the page never scrolls or zooms and nothing is cut off. Then check the keyboard and mouse still work as before.
|
|
37
|
+
|
|
38
|
+
Device mode can't show everything, sound on iPhones above all: tell the user, in a sentence, to try the game on a real phone after publishing.
|
|
39
|
+
|
|
40
|
+
## When it doesn't fit
|
|
41
|
+
Some games can't work on a phone, such as one that needs a full keyboard all the time. Tell the user why, do what still helps (screen fit, readable text) if they want it, and don't declare it.
|
|
42
|
+
|
|
43
|
+
## Declare it
|
|
44
|
+
Add `"touch"` to `input` in `juicy.json` (`"input": ["keyboard", "touch"]`, keeping what's there), and `"touch"` to `features` (`"features": ["touch"]`, adding to the list). JuicyJuicy can't detect this one from the files: `input` is what the site's chip reads and `features` is what ends the Upgrades panel's offer, so only add them once a full round plays by touch.
|
|
@@ -6,7 +6,7 @@ With `--json`, a failed command's last line is `{"ok": false, "error": {"code",
|
|
|
6
6
|
|
|
7
7
|
| Code | Meaning | Fix |
|
|
8
8
|
|---|---|---|
|
|
9
|
-
| `UNAUTHORIZED` | No saved login, or the token was revoked. | `npx -y juicyjuicy@0.
|
|
9
|
+
| `UNAUTHORIZED` | No saved login, or the token was revoked. | `npx -y juicyjuicy@0.16.0 login --json` (the user approves in the browser). In CI, set `JUICY_TOKEN`. |
|
|
10
10
|
| `EXPIRED_TOKEN` | The login code expired (10 minutes) or was used. | Run `login` again. |
|
|
11
11
|
| `ACCESS_DENIED` | The user clicked Deny. | Ask them, then run `login` again. |
|
|
12
12
|
|
|
@@ -1,11 +1,13 @@
|
|
|
1
1
|
# Bring all my JuicyJuicy games up to date
|
|
2
2
|
|
|
3
|
-
For "bring my JuicyJuicy games up to date", "fix the required updates", a **Needs update** badge on My games, or a `check` that warns `requirement-missing`. Sometimes JuicyJuicy makes a rule every game must follow (say, `juicy.json` listing its `features`). A game that doesn't follow it, or that JuicyJuicy hasn't checked yet, leaves the public lists after the deadline; its link keeps working.
|
|
3
|
+
For "bring my JuicyJuicy games up to date", "fix the required updates", a **Needs update** badge on My games, or a `check` or `publish` that warns `requirement-missing`. Sometimes JuicyJuicy makes a rule every game must follow (say, `juicy.json` listing its `features`). A game that doesn't follow it, or that JuicyJuicy hasn't checked yet, leaves the public lists after the deadline; its link keeps working.
|
|
4
|
+
|
|
5
|
+
To only say which games need an update, without checking or changing anything: `npx -y juicyjuicy@0.16.0 list --json` gives each of the user's games `needsUpdate`, one entry per required update (`title`, `deadline`, `phase` `enforced` once it's past the deadline, `notChecked` when JuicyJuicy hasn't checked the game yet, which step 1 settles); `[]` means none.
|
|
4
6
|
|
|
5
7
|
1. **Check every game at once.** Log in first (SKILL.md step 1), then run:
|
|
6
8
|
|
|
7
9
|
```bash
|
|
8
|
-
npx -y juicyjuicy@0.
|
|
10
|
+
npx -y juicyjuicy@0.16.0 check-games --json
|
|
9
11
|
```
|
|
10
12
|
|
|
11
13
|
JuicyJuicy checks the live version of each of the user's games on its side. It records which features each one has and, for a game whose `juicy.json` never listed its `features`, declares what it finds in the files (like cloud saves). Nothing is uploaded and no new version is made. The result has one entry per game in `games` (`features` with `on`/`off`/`unknown`, `requirements` with `passed`), `toFix` (the games that still need a code change, each with its `prompts`), `notReached` and `next`.
|
|
@@ -13,7 +15,7 @@ For "bring my JuicyJuicy games up to date", "fix the required updates", a **Need
|
|
|
13
15
|
2. **If `notReached` isn't empty**, run `check-games` again: it had no time for those games.
|
|
14
16
|
|
|
15
17
|
3. **For each game in `toFix`**, one at a time:
|
|
16
|
-
- Find its project, or download the live version with `npx -y juicyjuicy@0.
|
|
18
|
+
- Find its project, or download the live version with `npx -y juicyjuicy@0.16.0 pull <slug> --json` (SKILL.md, "Add a JuicyJuicy upgrade", step 1).
|
|
17
19
|
- Do what each prompt says, with the recipe it names when it names one.
|
|
18
20
|
- Make sure `juicy.json` has `features` (`[]` if the game has none of the upgrades).
|
|
19
21
|
- Check and publish an update as SKILL.md steps 5–7 say, with `--message "Required update: <what>"`.
|