@pixelvibing/cli 0.46.0 → 0.49.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/README.md +3 -2
- package/dist/pixel-vibing-cli.js +1088 -951
- package/package.json +1 -1
- package/skills/pixelvibing/SKILL.md +24 -8
package/dist/pixel-vibing-cli.js
CHANGED
|
@@ -576,10 +576,11 @@ function createWebApi(fetcher = (input, init2) => fetch(input, init2), base = AP
|
|
|
576
576
|
/** Every version of the guide, newest first, with how and when each was made (specs/platform-projects/ R2.10). */
|
|
577
577
|
guideVersions: async (projectId) => (await json2(`/projects/${id(projectId)}/style-guide/versions`)).versions,
|
|
578
578
|
reviseGuide: (projectId, instruction) => paying2(async () => (await json2(`/projects/${id(projectId)}/style-guide/revisions`, { method: "POST", body: { instruction } })).job),
|
|
579
|
-
/** A page of a project's assets, of one kind
|
|
580
|
-
assets: (projectId, kind = null, before = null) => {
|
|
579
|
+
/** A page of a project's assets, of one kind and carrying one tag when they are named. */
|
|
580
|
+
assets: (projectId, kind = null, before = null, tag2 = null) => {
|
|
581
581
|
const query = new URLSearchParams();
|
|
582
582
|
if (kind !== null) query.set("kind", kind);
|
|
583
|
+
if (tag2 !== null) query.set("tag", tag2);
|
|
583
584
|
if (before !== null) query.set("before", before);
|
|
584
585
|
const search = query.toString();
|
|
585
586
|
return json2(`/projects/${id(projectId)}/assets${search === "" ? "" : `?${search}`}`);
|
|
@@ -592,6 +593,10 @@ function createWebApi(fetcher = (input, init2) => fetch(input, init2), base = AP
|
|
|
592
593
|
},
|
|
593
594
|
/** One of a project's assets, by its id or its name. */
|
|
594
595
|
asset: (projectId, ref) => json2(`/projects/${id(projectId)}/assets/${id(ref)}`),
|
|
596
|
+
/** The project's tags for concept art: the offered ones first, then its own (specs/concept-art/ R3.1). */
|
|
597
|
+
tags: async (projectId) => (await json2(`/projects/${id(projectId)}/tags`)).tags,
|
|
598
|
+
/** A concept art's tags replaced by these, a tag the project has not held made (R3.2). */
|
|
599
|
+
tagAsset: (projectId, ref, tags) => json2(`/projects/${id(projectId)}/assets/${id(ref)}/tags`, { method: "PUT", body: { tags } }),
|
|
595
600
|
/** Keep a PNG for the account, answering its id. */
|
|
596
601
|
uploadImage: async (png) => (await json2("/images", { method: "POST", bytes: { data: png, type: "image/png" } })).id,
|
|
597
602
|
/** Where the page shows a kept image from; the cookie carries the account. */
|
|
@@ -1066,1019 +1071,1117 @@ function describedInterface(manifest2, kept, earlier) {
|
|
|
1066
1071
|
next[name] = { meta: { kind: "ui", ...piece.nineGrid === void 0 ? {} : { nineGrid: piece.nineGrid, scale: "stretch" } } };
|
|
1067
1072
|
return next;
|
|
1068
1073
|
}
|
|
1069
|
-
const
|
|
1070
|
-
const
|
|
1071
|
-
const
|
|
1072
|
-
const
|
|
1073
|
-
const
|
|
1074
|
-
const
|
|
1075
|
-
|
|
1076
|
-
|
|
1077
|
-
|
|
1078
|
-
const AUDIO = 2;
|
|
1079
|
-
const INTERACT = 4;
|
|
1080
|
-
const EXTRA = 8;
|
|
1081
|
-
const U16 = 65535;
|
|
1082
|
-
const MAX_LAYERS = 255;
|
|
1083
|
-
const NAMED$1 = /* @__PURE__ */ new Set(["x", "y", "z", "layers", "blocking", "zone", "audioZone", "interact"]);
|
|
1084
|
-
class SectorError extends Error {
|
|
1074
|
+
const TILE_SIZE = 256;
|
|
1075
|
+
const EAI_MAGIC = 1229018181;
|
|
1076
|
+
const EAI_VERSION = 1;
|
|
1077
|
+
const HEADER_SIZE$1 = 32;
|
|
1078
|
+
const ASSET_SIZE = 24;
|
|
1079
|
+
const ATLAS_SIZE = 12;
|
|
1080
|
+
class AssetIndexError extends Error {
|
|
1081
|
+
// Assigned in the body: the suites run under Node's strip-only TypeScript
|
|
1082
|
+
// mode, which rejects parameter properties (docs/notes.md n-0023).
|
|
1085
1083
|
constructor(message) {
|
|
1086
1084
|
super(message);
|
|
1087
|
-
this.name = "
|
|
1085
|
+
this.name = "AssetIndexError";
|
|
1088
1086
|
}
|
|
1089
1087
|
}
|
|
1090
|
-
function
|
|
1091
|
-
if (
|
|
1092
|
-
|
|
1088
|
+
function parseAssetIndex(buffer) {
|
|
1089
|
+
if (buffer.byteLength < HEADER_SIZE$1) {
|
|
1090
|
+
throw new AssetIndexError(`buffer too small: ${buffer.byteLength} < ${HEADER_SIZE$1}`);
|
|
1091
|
+
}
|
|
1092
|
+
const view = new DataView(buffer);
|
|
1093
|
+
const magic = view.getUint32(0, true);
|
|
1094
|
+
if (magic !== EAI_MAGIC) {
|
|
1095
|
+
throw new AssetIndexError(`bad magic 0x${magic.toString(16)} (expected 0x${EAI_MAGIC.toString(16)})`);
|
|
1096
|
+
}
|
|
1097
|
+
const version2 = view.getUint32(4, true);
|
|
1098
|
+
if (version2 !== EAI_VERSION) {
|
|
1099
|
+
throw new AssetIndexError(`unsupported version ${version2}, reader speaks v${EAI_VERSION}`);
|
|
1100
|
+
}
|
|
1101
|
+
const assetCount = view.getUint32(8, true);
|
|
1102
|
+
const atlasCount = view.getUint32(12, true);
|
|
1103
|
+
const assetsOffset = view.getUint32(16, true);
|
|
1104
|
+
const atlasesOffset = view.getUint32(20, true);
|
|
1105
|
+
const stringsOffset = view.getUint32(24, true);
|
|
1106
|
+
const stringsLength = view.getUint32(28, true);
|
|
1107
|
+
if (buffer.byteLength !== stringsOffset + stringsLength) {
|
|
1108
|
+
throw new AssetIndexError(`file size ${buffer.byteLength} ≠ header-declared ${stringsOffset + stringsLength}`);
|
|
1109
|
+
}
|
|
1110
|
+
if (assetsOffset < HEADER_SIZE$1) {
|
|
1111
|
+
throw new AssetIndexError(`assetsOffset ${assetsOffset} is inside the ${HEADER_SIZE$1}-byte header`);
|
|
1112
|
+
}
|
|
1113
|
+
if (assetsOffset + assetCount * ASSET_SIZE > atlasesOffset) {
|
|
1114
|
+
throw new AssetIndexError("asset section overlaps atlas section");
|
|
1115
|
+
}
|
|
1116
|
+
if (atlasesOffset + atlasCount * ATLAS_SIZE > stringsOffset) {
|
|
1117
|
+
throw new AssetIndexError("atlas section overlaps string pool");
|
|
1118
|
+
}
|
|
1119
|
+
const pool = new Pool(buffer, stringsOffset, stringsLength);
|
|
1120
|
+
const atlases = readAtlases(view, pool, atlasCount, atlasesOffset);
|
|
1121
|
+
const assets = readAssets(view, pool, assetCount, assetsOffset, atlases);
|
|
1122
|
+
return { version: version2, assets, atlases };
|
|
1093
1123
|
}
|
|
1094
|
-
|
|
1095
|
-
|
|
1096
|
-
|
|
1097
|
-
|
|
1098
|
-
|
|
1099
|
-
|
|
1100
|
-
|
|
1101
|
-
|
|
1102
|
-
|
|
1124
|
+
class Pool {
|
|
1125
|
+
buffer;
|
|
1126
|
+
offset;
|
|
1127
|
+
length;
|
|
1128
|
+
decoder = new TextDecoder("utf-8", { fatal: true });
|
|
1129
|
+
constructor(buffer, offset, length) {
|
|
1130
|
+
this.buffer = buffer;
|
|
1131
|
+
this.offset = offset;
|
|
1132
|
+
this.length = length;
|
|
1133
|
+
}
|
|
1134
|
+
read(what, nameOffset, nameLength) {
|
|
1135
|
+
if (nameOffset < this.offset || nameOffset + nameLength > this.offset + this.length) {
|
|
1136
|
+
throw new AssetIndexError(`${what}: name range [${nameOffset}, +${nameLength}] outside the string pool`);
|
|
1103
1137
|
}
|
|
1104
|
-
|
|
1105
|
-
|
|
1106
|
-
|
|
1107
|
-
|
|
1108
|
-
bytes.push(cell.layers.length);
|
|
1109
|
-
for (const layer of cell.layers) push16(u16(layer, "a layer id"));
|
|
1110
|
-
push16(u16(cell.blocking, "blocking"));
|
|
1111
|
-
push16(u16(cell.zone, "a zone"));
|
|
1112
|
-
if (cell.audioZone !== void 0) push16(u16(cell.audioZone, "an audio zone"));
|
|
1113
|
-
if (cell.interact !== void 0) push16(u16(cell.interact, "interact"));
|
|
1114
|
-
if (extra !== null) {
|
|
1115
|
-
push16(extra.length);
|
|
1116
|
-
for (const byte of extra) bytes.push(byte);
|
|
1138
|
+
try {
|
|
1139
|
+
return this.decoder.decode(new Uint8Array(this.buffer, nameOffset, nameLength));
|
|
1140
|
+
} catch (error) {
|
|
1141
|
+
throw new AssetIndexError(`${what}: bad UTF-8 name (${error.message})`);
|
|
1117
1142
|
}
|
|
1118
1143
|
}
|
|
1119
|
-
return Uint8Array.from(bytes);
|
|
1120
1144
|
}
|
|
1121
|
-
function
|
|
1122
|
-
|
|
1123
|
-
|
|
1124
|
-
|
|
1125
|
-
|
|
1126
|
-
|
|
1127
|
-
|
|
1128
|
-
|
|
1129
|
-
|
|
1130
|
-
|
|
1131
|
-
|
|
1132
|
-
|
|
1133
|
-
|
|
1134
|
-
|
|
1135
|
-
|
|
1136
|
-
|
|
1137
|
-
|
|
1138
|
-
|
|
1139
|
-
|
|
1140
|
-
|
|
1141
|
-
const flags = read8();
|
|
1142
|
-
if ((flags & PRESENT) === 0) {
|
|
1143
|
-
cells.push(void 0);
|
|
1144
|
-
continue;
|
|
1145
|
+
function readAtlases(view, pool, atlasCount, atlasesOffset) {
|
|
1146
|
+
const atlases = new Array(atlasCount);
|
|
1147
|
+
for (let index = 0; index < atlasCount; index += 1) {
|
|
1148
|
+
const base = atlasesOffset + index * ATLAS_SIZE;
|
|
1149
|
+
atlases[index] = {
|
|
1150
|
+
index,
|
|
1151
|
+
width: view.getUint16(base, true),
|
|
1152
|
+
height: view.getUint16(base + 2, true),
|
|
1153
|
+
name: pool.read(`atlas ${index}`, view.getUint32(base + 4, true), view.getUint32(base + 8, true))
|
|
1154
|
+
};
|
|
1155
|
+
}
|
|
1156
|
+
return atlases;
|
|
1157
|
+
}
|
|
1158
|
+
function readAssets(view, pool, assetCount, assetsOffset, atlases) {
|
|
1159
|
+
const assets = /* @__PURE__ */ new Map();
|
|
1160
|
+
for (let index = 0; index < assetCount; index += 1) {
|
|
1161
|
+
const base = assetsOffset + index * ASSET_SIZE;
|
|
1162
|
+
const name = pool.read(`asset ${index}`, view.getUint32(base, true), view.getUint16(base + 4, true));
|
|
1163
|
+
if (assets.has(name)) {
|
|
1164
|
+
throw new AssetIndexError(`duplicate asset name ${name}`);
|
|
1145
1165
|
}
|
|
1146
|
-
const
|
|
1147
|
-
const
|
|
1148
|
-
|
|
1149
|
-
|
|
1150
|
-
const zone = read16();
|
|
1151
|
-
const audioZone = (flags & AUDIO) === 0 ? void 0 : read16();
|
|
1152
|
-
const interact = (flags & INTERACT) === 0 ? void 0 : read16();
|
|
1153
|
-
let extra;
|
|
1154
|
-
if ((flags & EXTRA) !== 0) {
|
|
1155
|
-
const length = read16();
|
|
1156
|
-
need(length);
|
|
1157
|
-
let parsed2;
|
|
1158
|
-
try {
|
|
1159
|
-
parsed2 = JSON.parse(decoder.decode(bytes.subarray(at, at + length)));
|
|
1160
|
-
} catch {
|
|
1161
|
-
throw new SectorError("a tile's extra fields are not JSON");
|
|
1162
|
-
}
|
|
1163
|
-
if (typeof parsed2 !== "object" || parsed2 === null || Array.isArray(parsed2)) throw new SectorError("a tile's extra fields are not an object");
|
|
1164
|
-
const named = Object.keys(parsed2).find((key) => NAMED$1.has(key));
|
|
1165
|
-
if (named !== void 0) throw new SectorError(`a tile's extra fields name ${named}, which the sector holds itself`);
|
|
1166
|
-
extra = parsed2;
|
|
1167
|
-
at += length;
|
|
1166
|
+
const atlasIndex = view.getUint16(base + 6, true);
|
|
1167
|
+
const atlas = atlases[atlasIndex];
|
|
1168
|
+
if (atlas === void 0) {
|
|
1169
|
+
throw new AssetIndexError(`asset ${name}: atlasIndex ${atlasIndex} ≥ atlasCount ${atlases.length}`);
|
|
1168
1170
|
}
|
|
1169
|
-
|
|
1170
|
-
|
|
1171
|
-
|
|
1172
|
-
|
|
1173
|
-
|
|
1174
|
-
|
|
1175
|
-
|
|
1176
|
-
|
|
1171
|
+
const entry = {
|
|
1172
|
+
name,
|
|
1173
|
+
atlasIndex,
|
|
1174
|
+
x: view.getUint16(base + 8, true),
|
|
1175
|
+
y: view.getUint16(base + 10, true),
|
|
1176
|
+
width: view.getUint16(base + 12, true),
|
|
1177
|
+
height: view.getUint16(base + 14, true),
|
|
1178
|
+
offsetX: view.getInt16(base + 16, true),
|
|
1179
|
+
offsetY: view.getInt16(base + 18, true),
|
|
1180
|
+
flags: view.getUint16(base + 20, true)
|
|
1181
|
+
};
|
|
1182
|
+
if (entry.x + entry.width > atlas.width || entry.y + entry.height > atlas.height) {
|
|
1183
|
+
throw new AssetIndexError(
|
|
1184
|
+
`asset ${name}: rectangle ${entry.x},${entry.y} ${entry.width}x${entry.height} leaves atlas ${atlas.name} (${atlas.width}x${atlas.height})`
|
|
1185
|
+
);
|
|
1186
|
+
}
|
|
1187
|
+
assets.set(name, entry);
|
|
1177
1188
|
}
|
|
1178
|
-
|
|
1179
|
-
return cells;
|
|
1189
|
+
return assets;
|
|
1180
1190
|
}
|
|
1181
|
-
const
|
|
1182
|
-
|
|
1183
|
-
|
|
1184
|
-
|
|
1185
|
-
|
|
1186
|
-
|
|
1187
|
-
|
|
1188
|
-
|
|
1189
|
-
|
|
1190
|
-
|
|
1191
|
+
const U16_MAX = 65535;
|
|
1192
|
+
const I16_MIN = -32768;
|
|
1193
|
+
const I16_MAX = 32767;
|
|
1194
|
+
function writeAssetIndex(atlases, assets) {
|
|
1195
|
+
const encoder2 = new TextEncoder();
|
|
1196
|
+
const ordered = [...assets].sort((left, right) => compareBytes(encoder2, left.name, right.name));
|
|
1197
|
+
const seen = /* @__PURE__ */ new Set();
|
|
1198
|
+
for (const asset of ordered) {
|
|
1199
|
+
if (seen.has(asset.name)) throw new AssetIndexError(`duplicate asset name ${asset.name}`);
|
|
1200
|
+
seen.add(asset.name);
|
|
1201
|
+
if (asset.atlasIndex >= atlases.length) {
|
|
1202
|
+
throw new AssetIndexError(`asset ${asset.name}: atlasIndex ${asset.atlasIndex} ≥ atlasCount ${atlases.length}`);
|
|
1203
|
+
}
|
|
1204
|
+
checkU16(asset.name, { x: asset.x, y: asset.y, width: asset.width, height: asset.height, flags: asset.flags });
|
|
1205
|
+
checkI16(asset.name, { offsetX: asset.offsetX, offsetY: asset.offsetY });
|
|
1206
|
+
const atlas = atlases[asset.atlasIndex];
|
|
1207
|
+
if (asset.x + asset.width > atlas.width || asset.y + asset.height > atlas.height) {
|
|
1208
|
+
throw new AssetIndexError(`asset ${asset.name}: rectangle leaves atlas ${atlas.name} (${atlas.width}x${atlas.height})`);
|
|
1209
|
+
}
|
|
1210
|
+
}
|
|
1211
|
+
for (const atlas of atlases) {
|
|
1212
|
+
checkU16(atlas.name, { width: atlas.width, height: atlas.height });
|
|
1213
|
+
}
|
|
1214
|
+
const pool = [];
|
|
1215
|
+
const ranges = /* @__PURE__ */ new Map();
|
|
1216
|
+
const intern = (text2) => {
|
|
1217
|
+
const found = ranges.get(text2);
|
|
1218
|
+
if (found !== void 0) return found;
|
|
1219
|
+
const bytes = encoder2.encode(text2);
|
|
1220
|
+
const range = { offset: pool.length, length: bytes.length };
|
|
1221
|
+
if (range.length > U16_MAX) throw new AssetIndexError(`name too long: ${text2}`);
|
|
1222
|
+
pool.push(...bytes);
|
|
1223
|
+
ranges.set(text2, range);
|
|
1224
|
+
return range;
|
|
1191
1225
|
};
|
|
1226
|
+
for (const asset of ordered) intern(asset.name);
|
|
1227
|
+
for (const atlas of atlases) intern(atlas.name);
|
|
1228
|
+
const assetsOffset = HEADER_SIZE$1;
|
|
1229
|
+
const atlasesOffset = assetsOffset + ordered.length * ASSET_SIZE;
|
|
1230
|
+
const stringsOffset = atlasesOffset + atlases.length * ATLAS_SIZE;
|
|
1231
|
+
const buffer = new ArrayBuffer(stringsOffset + pool.length);
|
|
1232
|
+
const view = new DataView(buffer);
|
|
1233
|
+
view.setUint32(0, EAI_MAGIC, true);
|
|
1234
|
+
view.setUint32(4, EAI_VERSION, true);
|
|
1235
|
+
view.setUint32(8, ordered.length, true);
|
|
1236
|
+
view.setUint32(12, atlases.length, true);
|
|
1237
|
+
view.setUint32(16, assetsOffset, true);
|
|
1238
|
+
view.setUint32(20, atlasesOffset, true);
|
|
1239
|
+
view.setUint32(24, stringsOffset, true);
|
|
1240
|
+
view.setUint32(28, pool.length, true);
|
|
1241
|
+
ordered.forEach((asset, index) => {
|
|
1242
|
+
const base = assetsOffset + index * ASSET_SIZE;
|
|
1243
|
+
const range = ranges.get(asset.name);
|
|
1244
|
+
view.setUint32(base, stringsOffset + range.offset, true);
|
|
1245
|
+
view.setUint16(base + 4, range.length, true);
|
|
1246
|
+
view.setUint16(base + 6, asset.atlasIndex, true);
|
|
1247
|
+
view.setUint16(base + 8, asset.x, true);
|
|
1248
|
+
view.setUint16(base + 10, asset.y, true);
|
|
1249
|
+
view.setUint16(base + 12, asset.width, true);
|
|
1250
|
+
view.setUint16(base + 14, asset.height, true);
|
|
1251
|
+
view.setInt16(base + 16, asset.offsetX, true);
|
|
1252
|
+
view.setInt16(base + 18, asset.offsetY, true);
|
|
1253
|
+
view.setUint16(base + 20, asset.flags, true);
|
|
1254
|
+
});
|
|
1255
|
+
atlases.forEach((atlas, index) => {
|
|
1256
|
+
const base = atlasesOffset + index * ATLAS_SIZE;
|
|
1257
|
+
const range = ranges.get(atlas.name);
|
|
1258
|
+
view.setUint16(base, atlas.width, true);
|
|
1259
|
+
view.setUint16(base + 2, atlas.height, true);
|
|
1260
|
+
view.setUint32(base + 4, stringsOffset + range.offset, true);
|
|
1261
|
+
view.setUint32(base + 8, range.length, true);
|
|
1262
|
+
});
|
|
1263
|
+
new Uint8Array(buffer).set(new Uint8Array(pool), stringsOffset);
|
|
1264
|
+
return buffer;
|
|
1192
1265
|
}
|
|
1193
|
-
function
|
|
1194
|
-
|
|
1195
|
-
|
|
1196
|
-
|
|
1197
|
-
|
|
1198
|
-
|
|
1199
|
-
|
|
1200
|
-
|
|
1201
|
-
|
|
1202
|
-
|
|
1203
|
-
|
|
1204
|
-
|
|
1266
|
+
function compareBytes(encoder2, left, right) {
|
|
1267
|
+
const a = encoder2.encode(left);
|
|
1268
|
+
const b = encoder2.encode(right);
|
|
1269
|
+
for (let index = 0; index < Math.min(a.length, b.length); index += 1) {
|
|
1270
|
+
const difference = (a[index] ?? 0) - (b[index] ?? 0);
|
|
1271
|
+
if (difference !== 0) return difference;
|
|
1272
|
+
}
|
|
1273
|
+
return a.length - b.length;
|
|
1274
|
+
}
|
|
1275
|
+
function checkU16(what, fields) {
|
|
1276
|
+
for (const [field, value] of Object.entries(fields)) {
|
|
1277
|
+
if (!Number.isInteger(value) || value < 0 || value > U16_MAX) {
|
|
1278
|
+
throw new AssetIndexError(`${what}: ${field} ${value} does not fit in a u16`);
|
|
1279
|
+
}
|
|
1280
|
+
}
|
|
1281
|
+
}
|
|
1282
|
+
function checkI16(what, fields) {
|
|
1283
|
+
for (const [field, value] of Object.entries(fields)) {
|
|
1284
|
+
if (!Number.isInteger(value) || value < I16_MIN || value > I16_MAX) {
|
|
1285
|
+
throw new AssetIndexError(`${what}: ${field} ${value} does not fit in an i16`);
|
|
1286
|
+
}
|
|
1287
|
+
}
|
|
1288
|
+
}
|
|
1289
|
+
const METADATA_ENTRY = "meta.json";
|
|
1290
|
+
const METADATA_VERSION = 1;
|
|
1291
|
+
const ANIMATION_KEYS = [
|
|
1292
|
+
"idle",
|
|
1293
|
+
"stand-north",
|
|
1294
|
+
"stand-south",
|
|
1295
|
+
"stand-west",
|
|
1296
|
+
"stand-east",
|
|
1297
|
+
"walk-north",
|
|
1298
|
+
"walk-south",
|
|
1299
|
+
"walk-west",
|
|
1300
|
+
"walk-east",
|
|
1301
|
+
"attack-north",
|
|
1302
|
+
"attack-south",
|
|
1303
|
+
"attack-west",
|
|
1304
|
+
"attack-east",
|
|
1305
|
+
"icon"
|
|
1306
|
+
];
|
|
1307
|
+
const ANIMATION_KEY_SET = new Set(ANIMATION_KEYS);
|
|
1308
|
+
function isAnimationKey(key) {
|
|
1309
|
+
return ANIMATION_KEY_SET.has(key);
|
|
1310
|
+
}
|
|
1311
|
+
const ASSET_KINDS = ["outfit", "tile", "item", "effect", "ui"];
|
|
1312
|
+
const UI_SCALES = ["stretch", "repeat", "round"];
|
|
1313
|
+
const PLAY_EVERY_MIN_MS = 250;
|
|
1314
|
+
const PLAY_EVERY_MAX_MS = 6e5;
|
|
1315
|
+
const MAX_ANIMATION_NAME = 48;
|
|
1316
|
+
const ANIMATION_NAME = /^[a-z0-9]+(?:-[a-z0-9]+)*$/;
|
|
1317
|
+
function isAnimationName(name) {
|
|
1318
|
+
return name.length <= MAX_ANIMATION_NAME && ANIMATION_NAME.test(name);
|
|
1205
1319
|
}
|
|
1206
|
-
|
|
1207
|
-
|
|
1208
|
-
|
|
1209
|
-
class MirrorError extends Error {
|
|
1210
|
-
// Assigned in the body: Node's strip-only TypeScript mode rejects parameter
|
|
1211
|
-
// properties (docs/notes.md n-0023).
|
|
1320
|
+
class AssetMetadataError extends Error {
|
|
1321
|
+
// Assigned in the body: the suites run under Node's strip-only TypeScript
|
|
1322
|
+
// mode, which rejects parameter properties (docs/notes.md n-0023).
|
|
1212
1323
|
constructor(message) {
|
|
1213
1324
|
super(message);
|
|
1214
|
-
this.name = "
|
|
1325
|
+
this.name = "AssetMetadataError";
|
|
1215
1326
|
}
|
|
1216
1327
|
}
|
|
1217
|
-
function
|
|
1218
|
-
|
|
1219
|
-
return `${MIRROR_MAP_DIRECTORY}/${z}/${cx}-${cy}.bin`;
|
|
1220
|
-
}
|
|
1221
|
-
function isPlace(z, cx, cy) {
|
|
1222
|
-
const within = (value, last) => Number.isInteger(value) && value >= 0 && value <= last;
|
|
1223
|
-
return within(z, MAP_LEVELS.last) && z >= MAP_LEVELS.first && within(cx, OUTLINE_ACROSS - 1) && within(cy, OUTLINE_ACROSS - 1);
|
|
1224
|
-
}
|
|
1225
|
-
function isSize(value) {
|
|
1226
|
-
const side = (n) => Number.isInteger(n) && n >= 1 && n <= LARGEST_MAP_SIDE;
|
|
1227
|
-
return typeof value === "object" && value !== null && side(value.width) && side(value.height);
|
|
1228
|
-
}
|
|
1229
|
-
function mirrorIndexText(index) {
|
|
1230
|
-
return `${JSON.stringify(index, null, 2)}
|
|
1231
|
-
`;
|
|
1328
|
+
function isRecord$2(value) {
|
|
1329
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
1232
1330
|
}
|
|
1233
|
-
|
|
1234
|
-
|
|
1235
|
-
|
|
1236
|
-
|
|
1237
|
-
} catch {
|
|
1238
|
-
throw new MirrorError(`${MIRROR_INDEX_FILE} is not JSON`);
|
|
1331
|
+
const MAX_ANIMATION_FRAMES = 64;
|
|
1332
|
+
function readFrames(value, asset, animation) {
|
|
1333
|
+
if (!Array.isArray(value)) {
|
|
1334
|
+
throw new AssetMetadataError(`${asset}: animation ${animation} is not a list of frames`);
|
|
1239
1335
|
}
|
|
1240
|
-
|
|
1241
|
-
|
|
1242
|
-
if (index.format !== MIRROR_FORMAT)
|
|
1243
|
-
throw new MirrorError(`${MIRROR_INDEX_FILE} is in format ${String(index.format)}, and this reads format ${MIRROR_FORMAT}`);
|
|
1244
|
-
if (!Number.isInteger(index.revision) || index.revision < 0) throw new MirrorError(`${MIRROR_INDEX_FILE} names no revision`);
|
|
1245
|
-
if (!isSize(index.size) || !isSize(index.limit)) throw new MirrorError(`${MIRROR_INDEX_FILE} names no size or limit a map may have`);
|
|
1246
|
-
if (!Array.isArray(index.names) || !Array.isArray(index.sectors)) throw new MirrorError(`${MIRROR_INDEX_FILE} lists no levels or sectors`);
|
|
1247
|
-
for (const named of index.names) {
|
|
1248
|
-
if (typeof named?.name !== "string" || !isPlace(named.z, 0, 0)) throw new MirrorError(`${MIRROR_INDEX_FILE} names a level that is none`);
|
|
1336
|
+
if (value.length > MAX_ANIMATION_FRAMES) {
|
|
1337
|
+
throw new AssetMetadataError(`${asset}: animation ${animation} has ${value.length} frames, and an animation holds at most ${MAX_ANIMATION_FRAMES}`);
|
|
1249
1338
|
}
|
|
1250
|
-
|
|
1251
|
-
|
|
1252
|
-
|
|
1253
|
-
if (!isPlace(z, cx, cy) || !Number.isInteger(version2) || version2 < 1) {
|
|
1254
|
-
throw new MirrorError(`${MIRROR_INDEX_FILE} lists a sector that is none: ${JSON.stringify(sector)}`);
|
|
1255
|
-
}
|
|
1256
|
-
const key = `${z}:${cx}:${cy}`;
|
|
1257
|
-
if (cx * SECTOR_SIDE$1 >= index.size.width || cy * SECTOR_SIDE$1 >= index.size.height) {
|
|
1258
|
-
throw new MirrorError(`${MIRROR_INDEX_FILE} lists the sector ${key}, outside the map's size`);
|
|
1339
|
+
return value.map((raw, index) => {
|
|
1340
|
+
if (!isRecord$2(raw)) {
|
|
1341
|
+
throw new AssetMetadataError(`${asset}: animation ${animation} frame ${index} is not a frame`);
|
|
1259
1342
|
}
|
|
1260
|
-
|
|
1261
|
-
|
|
1262
|
-
|
|
1263
|
-
return index;
|
|
1264
|
-
}
|
|
1265
|
-
async function readMirroredMap(index, read, inflate2) {
|
|
1266
|
-
const tiles = [];
|
|
1267
|
-
for (const [z, cx, cy] of index.sectors) {
|
|
1268
|
-
const path = mirrorSectorPath(z, cx, cy);
|
|
1269
|
-
let cells;
|
|
1270
|
-
try {
|
|
1271
|
-
cells = decodeSector(await inflate2(await read(path)));
|
|
1272
|
-
} catch (error) {
|
|
1273
|
-
throw new MirrorError(`${path} is not a sector the map can read: ${error instanceof Error ? error.message : String(error)}`);
|
|
1343
|
+
const name = raw.asset;
|
|
1344
|
+
if (typeof name !== "string" || name === "") {
|
|
1345
|
+
throw new AssetMetadataError(`${asset}: animation ${animation} frame ${index} names no asset`);
|
|
1274
1346
|
}
|
|
1275
|
-
|
|
1276
|
-
|
|
1277
|
-
|
|
1278
|
-
|
|
1279
|
-
|
|
1280
|
-
tiles.push(tileOfCell(cell, x, y, z));
|
|
1347
|
+
const ms = raw.ms;
|
|
1348
|
+
if (typeof ms !== "number" || !Number.isInteger(ms) || ms < 0) {
|
|
1349
|
+
throw new AssetMetadataError(
|
|
1350
|
+
`${asset}: animation ${animation} frame ${index} has duration ${String(ms)}, which is not a whole number of milliseconds at or above zero`
|
|
1351
|
+
);
|
|
1281
1352
|
}
|
|
1282
|
-
|
|
1283
|
-
|
|
1284
|
-
return { width: index.size.width, height: index.size.height, tiles };
|
|
1353
|
+
return { asset: name, ms, flip: raw.flip === true };
|
|
1354
|
+
});
|
|
1285
1355
|
}
|
|
1286
|
-
function
|
|
1287
|
-
const
|
|
1288
|
-
|
|
1289
|
-
|
|
1290
|
-
|
|
1291
|
-
|
|
1356
|
+
function readAnimations(value, asset, kind) {
|
|
1357
|
+
const animations = /* @__PURE__ */ new Map();
|
|
1358
|
+
if (value === void 0) return animations;
|
|
1359
|
+
if (!isRecord$2(value)) {
|
|
1360
|
+
throw new AssetMetadataError(`${asset}: animations is not a map of name to frames`);
|
|
1361
|
+
}
|
|
1362
|
+
for (const [name, frames] of Object.entries(value)) {
|
|
1363
|
+
if (kind === "outfit" && !isAnimationKey(name)) {
|
|
1364
|
+
throw new AssetMetadataError(`${asset}: unknown animation ${name} (expected one of ${ANIMATION_KEYS.join(", ")})`);
|
|
1292
1365
|
}
|
|
1293
|
-
|
|
1294
|
-
|
|
1295
|
-
const key = `${z}:${cx}:${cy}`;
|
|
1296
|
-
let sector = sectors.get(key);
|
|
1297
|
-
if (sector === void 0) {
|
|
1298
|
-
sector = { z, cx, cy, cells: new Array(SECTOR_CELLS) };
|
|
1299
|
-
sectors.set(key, sector);
|
|
1366
|
+
if (kind !== "outfit" && !isAnimationName(name)) {
|
|
1367
|
+
throw new AssetMetadataError(`${asset}: animation ${name} is not a kebab-case name of at most ${MAX_ANIMATION_NAME} characters`);
|
|
1300
1368
|
}
|
|
1301
|
-
|
|
1302
|
-
}
|
|
1303
|
-
return [...sectors.values()].sort((a, b) => a.z - b.z || a.cy - b.cy || a.cx - b.cx);
|
|
1304
|
-
}
|
|
1305
|
-
class MapTextError extends Error {
|
|
1306
|
-
// Assigned in the body: the suites run under Node's strip-only TypeScript
|
|
1307
|
-
// mode, which rejects parameter properties (docs/notes.md n-0023).
|
|
1308
|
-
constructor(message) {
|
|
1309
|
-
super(message);
|
|
1310
|
-
this.name = "MapTextError";
|
|
1369
|
+
animations.set(name, readFrames(frames, asset, name));
|
|
1311
1370
|
}
|
|
1371
|
+
return animations;
|
|
1312
1372
|
}
|
|
1313
|
-
function
|
|
1314
|
-
|
|
1315
|
-
|
|
1316
|
-
|
|
1317
|
-
const
|
|
1318
|
-
|
|
1319
|
-
|
|
1320
|
-
|
|
1321
|
-
|
|
1322
|
-
|
|
1323
|
-
|
|
1324
|
-
let inString = false;
|
|
1325
|
-
let start = -1;
|
|
1326
|
-
for (let index = at + 1; index < text2.length; index += 1) {
|
|
1327
|
-
const char = text2[index];
|
|
1328
|
-
if (inString) {
|
|
1329
|
-
if (char === "\\") index += 1;
|
|
1330
|
-
else if (char === '"') inString = false;
|
|
1373
|
+
function readPlays(value, asset, animations, kind) {
|
|
1374
|
+
const plays = /* @__PURE__ */ new Map();
|
|
1375
|
+
if (value === void 0) return plays;
|
|
1376
|
+
if (!isRecord$2(value)) throw new AssetMetadataError(`${asset}: plays is not a map of animation to how it plays`);
|
|
1377
|
+
for (const [name, play] of Object.entries(value)) {
|
|
1378
|
+
if (!animations.has(name)) throw new AssetMetadataError(`${asset}: plays names ${name}, and there is no such animation`);
|
|
1379
|
+
if (!isRecord$2(play)) throw new AssetMetadataError(`${asset}: how ${name} plays is not an object`);
|
|
1380
|
+
if (kind === "outfit" && play.mode !== "loop")
|
|
1381
|
+
throw new AssetMetadataError(`${asset}: ${name} is an outfit's animation, which plays as the game drives it, in a loop`);
|
|
1382
|
+
if (play.mode === "loop" || play.mode === "change") {
|
|
1383
|
+
plays.set(name, { mode: play.mode });
|
|
1331
1384
|
continue;
|
|
1332
1385
|
}
|
|
1333
|
-
if (
|
|
1334
|
-
|
|
1335
|
-
|
|
1336
|
-
|
|
1337
|
-
|
|
1338
|
-
|
|
1339
|
-
depth -= 1;
|
|
1340
|
-
if (depth === 0 && start !== -1) {
|
|
1341
|
-
sources.push(text2.slice(start, index + 1));
|
|
1342
|
-
start = -1;
|
|
1386
|
+
if (play.mode === "every") {
|
|
1387
|
+
const ms = play.ms;
|
|
1388
|
+
if (typeof ms !== "number" || !Number.isInteger(ms) || ms < PLAY_EVERY_MIN_MS || ms > PLAY_EVERY_MAX_MS) {
|
|
1389
|
+
throw new AssetMetadataError(
|
|
1390
|
+
`${asset}: ${name} plays every ${String(ms)} ms, and an interval is a whole ${PLAY_EVERY_MIN_MS} to ${PLAY_EVERY_MAX_MS} ms`
|
|
1391
|
+
);
|
|
1343
1392
|
}
|
|
1344
|
-
|
|
1345
|
-
|
|
1393
|
+
plays.set(name, { mode: "every", ms });
|
|
1394
|
+
continue;
|
|
1346
1395
|
}
|
|
1396
|
+
throw new AssetMetadataError(`${asset}: ${name} plays as ${String(play.mode)}, and an animation plays as loop, every or change`);
|
|
1347
1397
|
}
|
|
1348
|
-
return
|
|
1349
|
-
}
|
|
1350
|
-
function isTile(value) {
|
|
1351
|
-
if (typeof value !== "object" || value === null) return false;
|
|
1352
|
-
const record = value;
|
|
1353
|
-
return typeof record.x === "number" && typeof record.y === "number" && typeof record.z === "number" && Array.isArray(record.layers);
|
|
1398
|
+
return plays;
|
|
1354
1399
|
}
|
|
1355
|
-
function
|
|
1356
|
-
|
|
1357
|
-
|
|
1358
|
-
|
|
1359
|
-
} catch (error) {
|
|
1360
|
-
throw new MapTextError(`${path} is not valid JSON: ${error.message}`);
|
|
1361
|
-
}
|
|
1362
|
-
if (typeof parsed2 !== "object" || parsed2 === null || !Array.isArray(parsed2.tiles)) {
|
|
1363
|
-
throw new MapTextError(`${path} holds no tiles array`);
|
|
1400
|
+
function readNineGrid(value, asset) {
|
|
1401
|
+
if (value === void 0) return void 0;
|
|
1402
|
+
if (!isRecord$2(value)) {
|
|
1403
|
+
throw new AssetMetadataError(`${asset}: nineGrid is not an object of four insets`);
|
|
1364
1404
|
}
|
|
1365
|
-
const
|
|
1366
|
-
const
|
|
1367
|
-
|
|
1368
|
-
|
|
1405
|
+
const insets = {};
|
|
1406
|
+
for (const edge of ["top", "right", "bottom", "left"]) {
|
|
1407
|
+
const inset = value[edge];
|
|
1408
|
+
if (inset === void 0) {
|
|
1409
|
+
throw new AssetMetadataError(`${asset}: nineGrid declares no ${edge} inset`);
|
|
1410
|
+
}
|
|
1411
|
+
if (typeof inset !== "number" || !Number.isInteger(inset) || inset < 0) {
|
|
1412
|
+
throw new AssetMetadataError(`${asset}: nineGrid ${edge} is ${String(inset)}, which is not a whole number of pixels at or above zero`);
|
|
1413
|
+
}
|
|
1414
|
+
insets[edge] = inset;
|
|
1369
1415
|
}
|
|
1370
|
-
|
|
1371
|
-
document.tiles.forEach((tile, index) => {
|
|
1372
|
-
if (!isTile(tile)) throw new MapTextError(`${path}: tile ${index} lacks x, y, z or layers`);
|
|
1373
|
-
byKey.set(tileKey(tile.x, tile.y, tile.z), { tile, source: sources[index] });
|
|
1374
|
-
});
|
|
1375
|
-
return { document, sources: byKey };
|
|
1416
|
+
return { top: insets.top, right: insets.right, bottom: insets.bottom, left: insets.left };
|
|
1376
1417
|
}
|
|
1377
|
-
|
|
1378
|
-
|
|
1379
|
-
|
|
1380
|
-
|
|
1381
|
-
if (tile[key] !== void 0) ordered[key] = tile[key];
|
|
1382
|
-
}
|
|
1383
|
-
for (const key of Object.keys(tile)) {
|
|
1384
|
-
if (!(key in ordered) && tile[key] !== void 0) ordered[key] = tile[key];
|
|
1418
|
+
function readScale(value, asset) {
|
|
1419
|
+
if (value === void 0) return void 0;
|
|
1420
|
+
if (typeof value !== "string" || !UI_SCALES.includes(value)) {
|
|
1421
|
+
throw new AssetMetadataError(`${asset}: scale ${String(value)} is not one of ${UI_SCALES.join(", ")}`);
|
|
1385
1422
|
}
|
|
1386
|
-
return
|
|
1423
|
+
return value;
|
|
1387
1424
|
}
|
|
1388
|
-
function
|
|
1389
|
-
|
|
1390
|
-
|
|
1391
|
-
|
|
1392
|
-
|
|
1393
|
-
|
|
1394
|
-
|
|
1425
|
+
function readCollide(value, asset) {
|
|
1426
|
+
if (value === void 0) return void 0;
|
|
1427
|
+
if (!isRecord$2(value)) {
|
|
1428
|
+
throw new AssetMetadataError(`${asset}: collide is not a box of x, y, w and h in cells`);
|
|
1429
|
+
}
|
|
1430
|
+
const box = {};
|
|
1431
|
+
for (const side of ["x", "y", "w", "h"]) {
|
|
1432
|
+
const cells = value[side];
|
|
1433
|
+
if (typeof cells !== "number" || !Number.isInteger(cells)) {
|
|
1434
|
+
throw new AssetMetadataError(`${asset}: collide ${side} is ${String(cells)}, which is not a whole number of cells`);
|
|
1395
1435
|
}
|
|
1436
|
+
box[side] = cells;
|
|
1396
1437
|
}
|
|
1397
|
-
|
|
1398
|
-
|
|
1399
|
-
if (key !== "width" && key !== "height" && key !== "tiles") rest[key] = next[key];
|
|
1438
|
+
if (box.w < 1 || box.h < 1) {
|
|
1439
|
+
throw new AssetMetadataError(`${asset}: collide is ${box.w}x${box.h} cells, and a box is at least one by one`);
|
|
1400
1440
|
}
|
|
1401
|
-
|
|
1402
|
-
const head = `{"width":${JSON.stringify(next.width)},"height":${JSON.stringify(next.height)},"tiles":[`;
|
|
1403
|
-
const body = lines.length === 0 ? "" : `
|
|
1404
|
-
${lines.join(",\n")}
|
|
1405
|
-
`;
|
|
1406
|
-
return { text: `${head}${body}]${trailer}}
|
|
1407
|
-
`, changed };
|
|
1408
|
-
}
|
|
1409
|
-
const SIGNATURE$1 = [137, 80, 78, 71, 13, 10, 26, 10];
|
|
1410
|
-
function isPng(bytes) {
|
|
1411
|
-
return bytes.length >= 24 && SIGNATURE$1.every((byte, index) => bytes[index] === byte);
|
|
1412
|
-
}
|
|
1413
|
-
function pngSize(bytes) {
|
|
1414
|
-
if (!isPng(bytes)) throw new RangeError("those bytes are not a PNG");
|
|
1415
|
-
const view = new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength);
|
|
1416
|
-
return { width: view.getUint32(16), height: view.getUint32(20) };
|
|
1417
|
-
}
|
|
1418
|
-
const GROUND_KINDS = /* @__PURE__ */ new Set(["ground", "floor", "wall", "cliff", "interior", "grass-stone"]);
|
|
1419
|
-
function isGroundName(name) {
|
|
1420
|
-
const { inner } = splitTileset(name);
|
|
1421
|
-
const kind = inner.split("/")[1] ?? "";
|
|
1422
|
-
return GROUND_KINDS.has(kind) || inner.includes("/wang-");
|
|
1441
|
+
return { x: box.x, y: box.y, w: box.w, h: box.h };
|
|
1423
1442
|
}
|
|
1424
|
-
const
|
|
1425
|
-
const
|
|
1426
|
-
const
|
|
1427
|
-
|
|
1428
|
-
|
|
1429
|
-
|
|
1430
|
-
|
|
1431
|
-
|
|
1432
|
-
|
|
1433
|
-
|
|
1434
|
-
|
|
1435
|
-
|
|
1436
|
-
|
|
1443
|
+
const MAX_DRAW_OFFSET = TILE_SIZE * 8;
|
|
1444
|
+
const MIN_DRAW_SCALE = 0.05;
|
|
1445
|
+
const MAX_DRAW_SCALE = 8;
|
|
1446
|
+
function readOffset(value, asset) {
|
|
1447
|
+
if (value === void 0) return void 0;
|
|
1448
|
+
if (!isRecord$2(value) || typeof value.x !== "number" || typeof value.y !== "number" || !Number.isFinite(value.x) || !Number.isFinite(value.y)) {
|
|
1449
|
+
throw new AssetMetadataError(`${asset}: offset is not an x and a y in pixels`);
|
|
1450
|
+
}
|
|
1451
|
+
for (const axis of ["x", "y"]) {
|
|
1452
|
+
const px = value[axis];
|
|
1453
|
+
if (Math.abs(px) > MAX_DRAW_OFFSET) {
|
|
1454
|
+
throw new AssetMetadataError(`${asset}: offset ${axis} is ${px} px, and an offset is within ${MAX_DRAW_OFFSET} px of the tile`);
|
|
1455
|
+
}
|
|
1437
1456
|
}
|
|
1457
|
+
return { x: value.x, y: value.y };
|
|
1438
1458
|
}
|
|
1439
|
-
function
|
|
1440
|
-
if (
|
|
1441
|
-
|
|
1459
|
+
function readDrawScale(value, asset) {
|
|
1460
|
+
if (value === void 0) return void 0;
|
|
1461
|
+
if (typeof value !== "number" || !Number.isFinite(value) || value <= 0) {
|
|
1462
|
+
throw new AssetMetadataError(`${asset}: drawScale is ${String(value)}, and a scale is a number above zero`);
|
|
1442
1463
|
}
|
|
1443
|
-
|
|
1444
|
-
|
|
1445
|
-
if (magic !== EAI_MAGIC) {
|
|
1446
|
-
throw new AssetIndexError(`bad magic 0x${magic.toString(16)} (expected 0x${EAI_MAGIC.toString(16)})`);
|
|
1464
|
+
if (value < MIN_DRAW_SCALE || value > MAX_DRAW_SCALE) {
|
|
1465
|
+
throw new AssetMetadataError(`${asset}: drawScale is ${value}, and a scale is between ${MIN_DRAW_SCALE} and ${MAX_DRAW_SCALE}`);
|
|
1447
1466
|
}
|
|
1448
|
-
|
|
1449
|
-
|
|
1450
|
-
|
|
1467
|
+
return value;
|
|
1468
|
+
}
|
|
1469
|
+
function readEntry(value, asset) {
|
|
1470
|
+
if (!isRecord$2(value)) {
|
|
1471
|
+
throw new AssetMetadataError(`${asset}: not an entry`);
|
|
1451
1472
|
}
|
|
1452
|
-
const
|
|
1453
|
-
|
|
1454
|
-
|
|
1455
|
-
const atlasesOffset = view.getUint32(20, true);
|
|
1456
|
-
const stringsOffset = view.getUint32(24, true);
|
|
1457
|
-
const stringsLength = view.getUint32(28, true);
|
|
1458
|
-
if (buffer.byteLength !== stringsOffset + stringsLength) {
|
|
1459
|
-
throw new AssetIndexError(`file size ${buffer.byteLength} ≠ header-declared ${stringsOffset + stringsLength}`);
|
|
1473
|
+
const kind = value.kind;
|
|
1474
|
+
if (typeof kind !== "string") {
|
|
1475
|
+
throw new AssetMetadataError(`${asset}: declares no kind`);
|
|
1460
1476
|
}
|
|
1461
|
-
if (
|
|
1462
|
-
throw new
|
|
1477
|
+
if (!ASSET_KINDS.includes(kind)) {
|
|
1478
|
+
throw new AssetMetadataError(`${asset}: unknown kind ${kind} (expected ${ASSET_KINDS.join(", ")})`);
|
|
1463
1479
|
}
|
|
1464
|
-
|
|
1465
|
-
|
|
1480
|
+
const minimap = value.minimap;
|
|
1481
|
+
if (minimap !== void 0 && (typeof minimap !== "number" || !Number.isInteger(minimap))) {
|
|
1482
|
+
throw new AssetMetadataError(`${asset}: minimap category is not a whole number`);
|
|
1466
1483
|
}
|
|
1467
|
-
if (
|
|
1468
|
-
throw new
|
|
1484
|
+
if (kind !== "ui" && (value.nineGrid !== void 0 || value.scale !== void 0)) {
|
|
1485
|
+
throw new AssetMetadataError(`${asset}: nineGrid and scale are interface fields, and this is a ${kind}`);
|
|
1469
1486
|
}
|
|
1470
|
-
|
|
1471
|
-
|
|
1472
|
-
|
|
1473
|
-
|
|
1487
|
+
if (kind !== "tile" && (value.collide !== void 0 || value.offset !== void 0 || value.drawScale !== void 0)) {
|
|
1488
|
+
throw new AssetMetadataError(`${asset}: collide, offset and drawScale are tile fields, and this is a ${kind}`);
|
|
1489
|
+
}
|
|
1490
|
+
const animations = readAnimations(value.animations, asset, kind);
|
|
1491
|
+
return {
|
|
1492
|
+
kind,
|
|
1493
|
+
animations,
|
|
1494
|
+
plays: readPlays(value.plays, asset, animations, kind),
|
|
1495
|
+
blocking: value.blocking === true,
|
|
1496
|
+
foreground: value.foreground === true,
|
|
1497
|
+
roof: value.roof === true,
|
|
1498
|
+
door: value.door === true,
|
|
1499
|
+
water: value.water === true,
|
|
1500
|
+
minimap: minimap ?? 0,
|
|
1501
|
+
collide: readCollide(value.collide, asset),
|
|
1502
|
+
offset: readOffset(value.offset, asset),
|
|
1503
|
+
drawScale: readDrawScale(value.drawScale, asset),
|
|
1504
|
+
nineGrid: readNineGrid(value.nineGrid, asset),
|
|
1505
|
+
scale: readScale(value.scale, asset)
|
|
1506
|
+
};
|
|
1474
1507
|
}
|
|
1475
|
-
|
|
1476
|
-
|
|
1477
|
-
|
|
1478
|
-
|
|
1479
|
-
|
|
1480
|
-
|
|
1481
|
-
this.buffer = buffer;
|
|
1482
|
-
this.offset = offset;
|
|
1483
|
-
this.length = length;
|
|
1508
|
+
function parseAssetMetadata(text2) {
|
|
1509
|
+
let parsed2;
|
|
1510
|
+
try {
|
|
1511
|
+
parsed2 = JSON.parse(text2);
|
|
1512
|
+
} catch (error) {
|
|
1513
|
+
throw new AssetMetadataError(`not JSON: ${error.message}`);
|
|
1484
1514
|
}
|
|
1485
|
-
|
|
1486
|
-
|
|
1487
|
-
throw new AssetIndexError(`${what}: name range [${nameOffset}, +${nameLength}] outside the string pool`);
|
|
1488
|
-
}
|
|
1489
|
-
try {
|
|
1490
|
-
return this.decoder.decode(new Uint8Array(this.buffer, nameOffset, nameLength));
|
|
1491
|
-
} catch (error) {
|
|
1492
|
-
throw new AssetIndexError(`${what}: bad UTF-8 name (${error.message})`);
|
|
1493
|
-
}
|
|
1515
|
+
if (!isRecord$2(parsed2)) {
|
|
1516
|
+
throw new AssetMetadataError("not an object");
|
|
1494
1517
|
}
|
|
1495
|
-
|
|
1496
|
-
|
|
1497
|
-
|
|
1498
|
-
|
|
1499
|
-
|
|
1500
|
-
|
|
1501
|
-
|
|
1502
|
-
width: view.getUint16(base, true),
|
|
1503
|
-
height: view.getUint16(base + 2, true),
|
|
1504
|
-
name: pool.read(`atlas ${index}`, view.getUint32(base + 4, true), view.getUint32(base + 8, true))
|
|
1505
|
-
};
|
|
1518
|
+
const version2 = parsed2.version;
|
|
1519
|
+
if (version2 !== METADATA_VERSION) {
|
|
1520
|
+
throw new AssetMetadataError(`unsupported version ${String(version2)}, reader speaks v${METADATA_VERSION}`);
|
|
1521
|
+
}
|
|
1522
|
+
const rawAssets = parsed2.assets;
|
|
1523
|
+
if (rawAssets !== void 0 && !isRecord$2(rawAssets)) {
|
|
1524
|
+
throw new AssetMetadataError("assets is not a map of name to entry");
|
|
1506
1525
|
}
|
|
1507
|
-
return atlases;
|
|
1508
|
-
}
|
|
1509
|
-
function readAssets(view, pool, assetCount, assetsOffset, atlases) {
|
|
1510
1526
|
const assets = /* @__PURE__ */ new Map();
|
|
1511
|
-
for (
|
|
1512
|
-
|
|
1513
|
-
|
|
1514
|
-
|
|
1515
|
-
|
|
1516
|
-
|
|
1517
|
-
|
|
1518
|
-
|
|
1519
|
-
|
|
1520
|
-
|
|
1527
|
+
for (const [name, entry] of Object.entries(rawAssets ?? {})) {
|
|
1528
|
+
assets.set(name, readEntry(entry, name));
|
|
1529
|
+
}
|
|
1530
|
+
return { version: version2, assets };
|
|
1531
|
+
}
|
|
1532
|
+
function writeAssetMetadata(metadata) {
|
|
1533
|
+
const assets = {};
|
|
1534
|
+
for (const [name, entry] of [...metadata.assets].sort(([a], [b]) => a < b ? -1 : a > b ? 1 : 0)) {
|
|
1535
|
+
const out = { kind: entry.kind };
|
|
1536
|
+
if (entry.blocking) out.blocking = true;
|
|
1537
|
+
if (entry.foreground) out.foreground = true;
|
|
1538
|
+
if (entry.roof) out.roof = true;
|
|
1539
|
+
if (entry.door) out.door = true;
|
|
1540
|
+
if (entry.water) out.water = true;
|
|
1541
|
+
if (entry.minimap !== 0) out.minimap = entry.minimap;
|
|
1542
|
+
if (entry.collide !== void 0) out.collide = entry.collide;
|
|
1543
|
+
if (entry.offset !== void 0) out.offset = entry.offset;
|
|
1544
|
+
if (entry.drawScale !== void 0) out.drawScale = entry.drawScale;
|
|
1545
|
+
if (entry.nineGrid !== void 0) out.nineGrid = entry.nineGrid;
|
|
1546
|
+
if (entry.scale !== void 0) out.scale = entry.scale;
|
|
1547
|
+
if (entry.animations.size > 0) {
|
|
1548
|
+
const animations = {};
|
|
1549
|
+
const ordered = [...entry.animations].sort(([a], [b]) => a < b ? -1 : a > b ? 1 : 0);
|
|
1550
|
+
for (const [animation, frames] of ordered) {
|
|
1551
|
+
animations[animation] = frames.map((frame) => frame.flip ? { asset: frame.asset, ms: frame.ms, flip: true } : { asset: frame.asset, ms: frame.ms });
|
|
1552
|
+
}
|
|
1553
|
+
out.animations = animations;
|
|
1521
1554
|
}
|
|
1522
|
-
|
|
1523
|
-
|
|
1524
|
-
|
|
1525
|
-
|
|
1526
|
-
y: view.getUint16(base + 10, true),
|
|
1527
|
-
width: view.getUint16(base + 12, true),
|
|
1528
|
-
height: view.getUint16(base + 14, true),
|
|
1529
|
-
offsetX: view.getInt16(base + 16, true),
|
|
1530
|
-
offsetY: view.getInt16(base + 18, true),
|
|
1531
|
-
flags: view.getUint16(base + 20, true)
|
|
1532
|
-
};
|
|
1533
|
-
if (entry.x + entry.width > atlas.width || entry.y + entry.height > atlas.height) {
|
|
1534
|
-
throw new AssetIndexError(
|
|
1535
|
-
`asset ${name}: rectangle ${entry.x},${entry.y} ${entry.width}x${entry.height} leaves atlas ${atlas.name} (${atlas.width}x${atlas.height})`
|
|
1536
|
-
);
|
|
1555
|
+
if (entry.plays.size > 0) {
|
|
1556
|
+
const plays = {};
|
|
1557
|
+
for (const [animation, play] of [...entry.plays].sort(([a], [b]) => a < b ? -1 : a > b ? 1 : 0)) plays[animation] = play;
|
|
1558
|
+
out.plays = plays;
|
|
1537
1559
|
}
|
|
1538
|
-
assets
|
|
1560
|
+
assets[name] = out;
|
|
1539
1561
|
}
|
|
1540
|
-
return assets
|
|
1562
|
+
return `${JSON.stringify({ version: metadata.version, assets }, null, 2)}
|
|
1563
|
+
`;
|
|
1541
1564
|
}
|
|
1542
|
-
|
|
1543
|
-
|
|
1544
|
-
|
|
1545
|
-
|
|
1546
|
-
|
|
1547
|
-
|
|
1548
|
-
|
|
1549
|
-
|
|
1550
|
-
|
|
1551
|
-
|
|
1552
|
-
|
|
1553
|
-
|
|
1565
|
+
class IdMapError extends Error {
|
|
1566
|
+
// Assigned in the body: the suites run under Node's strip-only TypeScript
|
|
1567
|
+
// mode, which rejects parameter properties (docs/notes.md n-0023).
|
|
1568
|
+
constructor(message) {
|
|
1569
|
+
super(message);
|
|
1570
|
+
this.name = "IdMapError";
|
|
1571
|
+
}
|
|
1572
|
+
}
|
|
1573
|
+
const SECTIONS = ["looktypes", "frames"];
|
|
1574
|
+
const WHOLE_NUMBER = /^(0|[1-9][0-9]*)$/;
|
|
1575
|
+
const FRAME_KEY = /^(0|[1-9][0-9]*):(0|[1-9][0-9]*)$/;
|
|
1576
|
+
function parseIdMap(raw) {
|
|
1577
|
+
const record = asObject(raw, "the id map");
|
|
1578
|
+
for (const section2 of Object.keys(record)) {
|
|
1579
|
+
if (!SECTIONS.includes(section2)) {
|
|
1580
|
+
throw new IdMapError(`unknown section ${section2}: the map has looktypes and frames`);
|
|
1554
1581
|
}
|
|
1555
|
-
|
|
1556
|
-
|
|
1557
|
-
|
|
1558
|
-
|
|
1559
|
-
|
|
1582
|
+
}
|
|
1583
|
+
const looktypes = /* @__PURE__ */ new Map();
|
|
1584
|
+
const frames = /* @__PURE__ */ new Map();
|
|
1585
|
+
const names = /* @__PURE__ */ new Set();
|
|
1586
|
+
for (const [key, value] of Object.entries(section(record, "looktypes"))) {
|
|
1587
|
+
if (!WHOLE_NUMBER.test(key)) {
|
|
1588
|
+
throw new IdMapError(`looktype key ${key} is not a whole number`);
|
|
1560
1589
|
}
|
|
1590
|
+
const name = assetName(value, `looktype ${key}`);
|
|
1591
|
+
looktypes.set(Number(key), name);
|
|
1592
|
+
names.add(name);
|
|
1561
1593
|
}
|
|
1562
|
-
for (const
|
|
1563
|
-
|
|
1594
|
+
for (const [key, value] of Object.entries(section(record, "frames"))) {
|
|
1595
|
+
if (!FRAME_KEY.test(key)) {
|
|
1596
|
+
throw new IdMapError(`frame key ${key} is not packId:frameId`);
|
|
1597
|
+
}
|
|
1598
|
+
const name = assetName(value, `frame ${key}`);
|
|
1599
|
+
frames.set(key, name);
|
|
1600
|
+
names.add(name);
|
|
1564
1601
|
}
|
|
1565
|
-
|
|
1566
|
-
|
|
1567
|
-
|
|
1568
|
-
|
|
1569
|
-
|
|
1570
|
-
|
|
1571
|
-
const range = { offset: pool.length, length: bytes.length };
|
|
1572
|
-
if (range.length > U16_MAX) throw new AssetIndexError(`name too long: ${text2}`);
|
|
1573
|
-
pool.push(...bytes);
|
|
1574
|
-
ranges.set(text2, range);
|
|
1575
|
-
return range;
|
|
1602
|
+
return {
|
|
1603
|
+
nameForLooktype: (looktype) => looktypes.get(looktype) ?? null,
|
|
1604
|
+
nameForFrame: (packId, frameId) => frames.get(`${packId}:${frameId}`) ?? null,
|
|
1605
|
+
names,
|
|
1606
|
+
looktypeCount: looktypes.size,
|
|
1607
|
+
frameCount: frames.size
|
|
1576
1608
|
};
|
|
1577
|
-
for (const asset of ordered) intern(asset.name);
|
|
1578
|
-
for (const atlas of atlases) intern(atlas.name);
|
|
1579
|
-
const assetsOffset = HEADER_SIZE$1;
|
|
1580
|
-
const atlasesOffset = assetsOffset + ordered.length * ASSET_SIZE;
|
|
1581
|
-
const stringsOffset = atlasesOffset + atlases.length * ATLAS_SIZE;
|
|
1582
|
-
const buffer = new ArrayBuffer(stringsOffset + pool.length);
|
|
1583
|
-
const view = new DataView(buffer);
|
|
1584
|
-
view.setUint32(0, EAI_MAGIC, true);
|
|
1585
|
-
view.setUint32(4, EAI_VERSION, true);
|
|
1586
|
-
view.setUint32(8, ordered.length, true);
|
|
1587
|
-
view.setUint32(12, atlases.length, true);
|
|
1588
|
-
view.setUint32(16, assetsOffset, true);
|
|
1589
|
-
view.setUint32(20, atlasesOffset, true);
|
|
1590
|
-
view.setUint32(24, stringsOffset, true);
|
|
1591
|
-
view.setUint32(28, pool.length, true);
|
|
1592
|
-
ordered.forEach((asset, index) => {
|
|
1593
|
-
const base = assetsOffset + index * ASSET_SIZE;
|
|
1594
|
-
const range = ranges.get(asset.name);
|
|
1595
|
-
view.setUint32(base, stringsOffset + range.offset, true);
|
|
1596
|
-
view.setUint16(base + 4, range.length, true);
|
|
1597
|
-
view.setUint16(base + 6, asset.atlasIndex, true);
|
|
1598
|
-
view.setUint16(base + 8, asset.x, true);
|
|
1599
|
-
view.setUint16(base + 10, asset.y, true);
|
|
1600
|
-
view.setUint16(base + 12, asset.width, true);
|
|
1601
|
-
view.setUint16(base + 14, asset.height, true);
|
|
1602
|
-
view.setInt16(base + 16, asset.offsetX, true);
|
|
1603
|
-
view.setInt16(base + 18, asset.offsetY, true);
|
|
1604
|
-
view.setUint16(base + 20, asset.flags, true);
|
|
1605
|
-
});
|
|
1606
|
-
atlases.forEach((atlas, index) => {
|
|
1607
|
-
const base = atlasesOffset + index * ATLAS_SIZE;
|
|
1608
|
-
const range = ranges.get(atlas.name);
|
|
1609
|
-
view.setUint16(base, atlas.width, true);
|
|
1610
|
-
view.setUint16(base + 2, atlas.height, true);
|
|
1611
|
-
view.setUint32(base + 4, stringsOffset + range.offset, true);
|
|
1612
|
-
view.setUint32(base + 8, range.length, true);
|
|
1613
|
-
});
|
|
1614
|
-
new Uint8Array(buffer).set(new Uint8Array(pool), stringsOffset);
|
|
1615
|
-
return buffer;
|
|
1616
|
-
}
|
|
1617
|
-
function compareBytes(encoder2, left, right) {
|
|
1618
|
-
const a = encoder2.encode(left);
|
|
1619
|
-
const b = encoder2.encode(right);
|
|
1620
|
-
for (let index = 0; index < Math.min(a.length, b.length); index += 1) {
|
|
1621
|
-
const difference = (a[index] ?? 0) - (b[index] ?? 0);
|
|
1622
|
-
if (difference !== 0) return difference;
|
|
1623
|
-
}
|
|
1624
|
-
return a.length - b.length;
|
|
1625
1609
|
}
|
|
1626
|
-
function
|
|
1627
|
-
|
|
1628
|
-
|
|
1629
|
-
|
|
1630
|
-
}
|
|
1610
|
+
function checkNamesAgainstIndex(map, index, declared = /* @__PURE__ */ new Set()) {
|
|
1611
|
+
const absent = [...map.names].filter((name) => !index.assets.has(name) && !declared.has(name)).sort();
|
|
1612
|
+
if (absent.length > 0) {
|
|
1613
|
+
throw new IdMapError(`the id map names assets the index does not hold: ${absent.join(", ")}`);
|
|
1631
1614
|
}
|
|
1632
1615
|
}
|
|
1633
|
-
function
|
|
1634
|
-
|
|
1635
|
-
|
|
1636
|
-
throw new AssetIndexError(`${what}: ${field} ${value} does not fit in an i16`);
|
|
1637
|
-
}
|
|
1616
|
+
function asObject(value, what) {
|
|
1617
|
+
if (value === null || typeof value !== "object" || Array.isArray(value)) {
|
|
1618
|
+
throw new IdMapError(`${what} must be a JSON object`);
|
|
1638
1619
|
}
|
|
1620
|
+
return value;
|
|
1639
1621
|
}
|
|
1640
|
-
|
|
1641
|
-
const
|
|
1642
|
-
|
|
1643
|
-
"idle",
|
|
1644
|
-
"stand-north",
|
|
1645
|
-
"stand-south",
|
|
1646
|
-
"stand-west",
|
|
1647
|
-
"stand-east",
|
|
1648
|
-
"walk-north",
|
|
1649
|
-
"walk-south",
|
|
1650
|
-
"walk-west",
|
|
1651
|
-
"walk-east",
|
|
1652
|
-
"attack-north",
|
|
1653
|
-
"attack-south",
|
|
1654
|
-
"attack-west",
|
|
1655
|
-
"attack-east",
|
|
1656
|
-
"icon"
|
|
1657
|
-
];
|
|
1658
|
-
const ANIMATION_KEY_SET = new Set(ANIMATION_KEYS);
|
|
1659
|
-
function isAnimationKey(key) {
|
|
1660
|
-
return ANIMATION_KEY_SET.has(key);
|
|
1622
|
+
function section(record, name) {
|
|
1623
|
+
const value = record[name];
|
|
1624
|
+
return value === void 0 ? {} : asObject(value, `${name}`);
|
|
1661
1625
|
}
|
|
1662
|
-
|
|
1663
|
-
|
|
1664
|
-
|
|
1665
|
-
|
|
1666
|
-
|
|
1667
|
-
const ANIMATION_NAME = /^[a-z0-9]+(?:-[a-z0-9]+)*$/;
|
|
1668
|
-
function isAnimationName(name) {
|
|
1669
|
-
return name.length <= MAX_ANIMATION_NAME && ANIMATION_NAME.test(name);
|
|
1626
|
+
function assetName(value, what) {
|
|
1627
|
+
if (typeof value !== "string" || value.trim() === "") {
|
|
1628
|
+
throw new IdMapError(`${what}: the asset name must be a non-empty string`);
|
|
1629
|
+
}
|
|
1630
|
+
return value;
|
|
1670
1631
|
}
|
|
1671
|
-
|
|
1672
|
-
|
|
1673
|
-
|
|
1632
|
+
const EAC_MAGIC = 826491205;
|
|
1633
|
+
const EAC_VERSION = 1;
|
|
1634
|
+
const INDEX_ENTRY = "assets.eai";
|
|
1635
|
+
const ID_MAP_ENTRY = "id-map.json";
|
|
1636
|
+
const HEADER_SIZE = 32;
|
|
1637
|
+
const FLAG_MASKED = 1;
|
|
1638
|
+
const MASK_KEY = Uint8Array.from([69, 116, 101, 114, 110, 105, 116, 121, 77, 69, 45, 115, 101, 97, 108, 33]);
|
|
1639
|
+
class ContainerError extends Error {
|
|
1640
|
+
// Assigned in the body: Node's strip-only TypeScript mode rejects parameter
|
|
1641
|
+
// properties (docs/notes.md n-0023).
|
|
1674
1642
|
constructor(message) {
|
|
1675
1643
|
super(message);
|
|
1676
|
-
this.name = "
|
|
1644
|
+
this.name = "ContainerError";
|
|
1677
1645
|
}
|
|
1678
1646
|
}
|
|
1679
|
-
function
|
|
1680
|
-
|
|
1647
|
+
function maskByte(value, offset) {
|
|
1648
|
+
const key = MASK_KEY[offset % MASK_KEY.length];
|
|
1649
|
+
const drift = Math.imul(offset, 31) + (offset >>> 5) & 255;
|
|
1650
|
+
return (value ^ key ^ drift) & 255;
|
|
1681
1651
|
}
|
|
1682
|
-
|
|
1683
|
-
|
|
1684
|
-
|
|
1685
|
-
throw new AssetMetadataError(`${asset}: animation ${animation} is not a list of frames`);
|
|
1652
|
+
function maskInto(target, start, source) {
|
|
1653
|
+
for (let index = 0; index < source.length; index += 1) {
|
|
1654
|
+
target[start + index] = maskByte(source[index], start + index);
|
|
1686
1655
|
}
|
|
1687
|
-
|
|
1688
|
-
|
|
1656
|
+
}
|
|
1657
|
+
function sealContainer(entries) {
|
|
1658
|
+
const seen = /* @__PURE__ */ new Set();
|
|
1659
|
+
for (const entry of entries) {
|
|
1660
|
+
if (seen.has(entry.name)) throw new ContainerError(`duplicate entry name ${entry.name}`);
|
|
1661
|
+
seen.add(entry.name);
|
|
1689
1662
|
}
|
|
1690
|
-
|
|
1691
|
-
|
|
1692
|
-
|
|
1693
|
-
|
|
1694
|
-
|
|
1695
|
-
|
|
1696
|
-
|
|
1697
|
-
|
|
1698
|
-
|
|
1699
|
-
|
|
1700
|
-
|
|
1701
|
-
|
|
1702
|
-
|
|
1703
|
-
|
|
1704
|
-
|
|
1663
|
+
const encoder2 = new TextEncoder();
|
|
1664
|
+
const ordered = [...entries].sort((left, right) => left.name < right.name ? -1 : left.name > right.name ? 1 : 0);
|
|
1665
|
+
const names = ordered.map((entry) => encoder2.encode(entry.name));
|
|
1666
|
+
const tocLength = names.reduce((total, name) => total + 2 + name.length + 8, 0);
|
|
1667
|
+
const tocOffset = HEADER_SIZE;
|
|
1668
|
+
const payloadOffset = tocOffset + tocLength;
|
|
1669
|
+
const payloadLength = ordered.reduce((total, entry) => total + entry.bytes.length, 0);
|
|
1670
|
+
const out = new Uint8Array(payloadOffset + payloadLength);
|
|
1671
|
+
const view = new DataView(out.buffer);
|
|
1672
|
+
view.setUint32(0, EAC_MAGIC, true);
|
|
1673
|
+
view.setUint32(4, EAC_VERSION, true);
|
|
1674
|
+
view.setUint32(8, ordered.length, true);
|
|
1675
|
+
view.setUint32(12, tocOffset, true);
|
|
1676
|
+
view.setUint32(16, tocLength, true);
|
|
1677
|
+
view.setUint32(20, payloadOffset, true);
|
|
1678
|
+
view.setUint32(24, FLAG_MASKED, true);
|
|
1679
|
+
view.setUint32(28, 0, true);
|
|
1680
|
+
const toc = new Uint8Array(tocLength);
|
|
1681
|
+
const tocView = new DataView(toc.buffer);
|
|
1682
|
+
let cursor = 0;
|
|
1683
|
+
let entryOffset = payloadOffset;
|
|
1684
|
+
ordered.forEach((entry, index) => {
|
|
1685
|
+
const name = names[index];
|
|
1686
|
+
tocView.setUint16(cursor, name.length, true);
|
|
1687
|
+
cursor += 2;
|
|
1688
|
+
toc.set(name, cursor);
|
|
1689
|
+
cursor += name.length;
|
|
1690
|
+
tocView.setUint32(cursor, entryOffset, true);
|
|
1691
|
+
cursor += 4;
|
|
1692
|
+
tocView.setUint32(cursor, entry.bytes.length, true);
|
|
1693
|
+
cursor += 4;
|
|
1694
|
+
maskInto(out, entryOffset, entry.bytes);
|
|
1695
|
+
entryOffset += entry.bytes.length;
|
|
1705
1696
|
});
|
|
1697
|
+
maskInto(out, tocOffset, toc);
|
|
1698
|
+
return out;
|
|
1706
1699
|
}
|
|
1707
|
-
function
|
|
1708
|
-
const
|
|
1709
|
-
|
|
1710
|
-
|
|
1711
|
-
|
|
1712
|
-
|
|
1713
|
-
|
|
1714
|
-
|
|
1715
|
-
|
|
1716
|
-
|
|
1717
|
-
|
|
1718
|
-
|
|
1719
|
-
|
|
1720
|
-
|
|
1700
|
+
function tileSpritePlacement(tx, ty, width, height, tileSize = TILE_SIZE) {
|
|
1701
|
+
const overhangX = (width - tileSize) / 2;
|
|
1702
|
+
const overhangY = (height - tileSize) / 2;
|
|
1703
|
+
return { x: tx * tileSize - overhangX, y: ty * tileSize - overhangY };
|
|
1704
|
+
}
|
|
1705
|
+
function placeTileSprite(tx, ty, width, height, options = {}) {
|
|
1706
|
+
const tileSize = options.tileSize ?? TILE_SIZE;
|
|
1707
|
+
const scale = options.drawScale ?? 1;
|
|
1708
|
+
const scaledWidth = width * scale;
|
|
1709
|
+
const scaledHeight = height * scale;
|
|
1710
|
+
const centred = tileSpritePlacement(tx, ty, scaledWidth, scaledHeight, tileSize);
|
|
1711
|
+
return {
|
|
1712
|
+
x: centred.x + (options.offset?.x ?? 0),
|
|
1713
|
+
y: centred.y + (options.offset?.y ?? 0),
|
|
1714
|
+
width: scaledWidth,
|
|
1715
|
+
height: scaledHeight
|
|
1716
|
+
};
|
|
1717
|
+
}
|
|
1718
|
+
class MapTextError extends Error {
|
|
1719
|
+
// Assigned in the body: the suites run under Node's strip-only TypeScript
|
|
1720
|
+
// mode, which rejects parameter properties (docs/notes.md n-0023).
|
|
1721
|
+
constructor(message) {
|
|
1722
|
+
super(message);
|
|
1723
|
+
this.name = "MapTextError";
|
|
1721
1724
|
}
|
|
1722
|
-
return animations;
|
|
1723
1725
|
}
|
|
1724
|
-
function
|
|
1725
|
-
|
|
1726
|
-
|
|
1727
|
-
|
|
1728
|
-
|
|
1729
|
-
|
|
1730
|
-
|
|
1731
|
-
|
|
1732
|
-
|
|
1733
|
-
|
|
1734
|
-
|
|
1726
|
+
function tileKey(x, y, z) {
|
|
1727
|
+
return `${x}:${y}:${z}`;
|
|
1728
|
+
}
|
|
1729
|
+
function scanTileSources(text2) {
|
|
1730
|
+
const key = '"tiles"';
|
|
1731
|
+
let at = text2.indexOf(key);
|
|
1732
|
+
if (at === -1) return [];
|
|
1733
|
+
at = text2.indexOf("[", at + key.length);
|
|
1734
|
+
if (at === -1) return [];
|
|
1735
|
+
const sources = [];
|
|
1736
|
+
let depth = 0;
|
|
1737
|
+
let inString = false;
|
|
1738
|
+
let start = -1;
|
|
1739
|
+
for (let index = at + 1; index < text2.length; index += 1) {
|
|
1740
|
+
const char = text2[index];
|
|
1741
|
+
if (inString) {
|
|
1742
|
+
if (char === "\\") index += 1;
|
|
1743
|
+
else if (char === '"') inString = false;
|
|
1735
1744
|
continue;
|
|
1736
1745
|
}
|
|
1737
|
-
if (
|
|
1738
|
-
|
|
1739
|
-
|
|
1740
|
-
|
|
1741
|
-
|
|
1742
|
-
|
|
1746
|
+
if (char === '"') {
|
|
1747
|
+
inString = true;
|
|
1748
|
+
} else if (char === "{") {
|
|
1749
|
+
if (depth === 0) start = index;
|
|
1750
|
+
depth += 1;
|
|
1751
|
+
} else if (char === "}") {
|
|
1752
|
+
depth -= 1;
|
|
1753
|
+
if (depth === 0 && start !== -1) {
|
|
1754
|
+
sources.push(text2.slice(start, index + 1));
|
|
1755
|
+
start = -1;
|
|
1743
1756
|
}
|
|
1744
|
-
|
|
1745
|
-
|
|
1746
|
-
}
|
|
1747
|
-
throw new AssetMetadataError(`${asset}: ${name} plays as ${String(play.mode)}, and an animation plays as loop, every or change`);
|
|
1748
|
-
}
|
|
1749
|
-
return plays;
|
|
1750
|
-
}
|
|
1751
|
-
function readNineGrid(value, asset) {
|
|
1752
|
-
if (value === void 0) return void 0;
|
|
1753
|
-
if (!isRecord$2(value)) {
|
|
1754
|
-
throw new AssetMetadataError(`${asset}: nineGrid is not an object of four insets`);
|
|
1755
|
-
}
|
|
1756
|
-
const insets = {};
|
|
1757
|
-
for (const edge of ["top", "right", "bottom", "left"]) {
|
|
1758
|
-
const inset = value[edge];
|
|
1759
|
-
if (inset === void 0) {
|
|
1760
|
-
throw new AssetMetadataError(`${asset}: nineGrid declares no ${edge} inset`);
|
|
1761
|
-
}
|
|
1762
|
-
if (typeof inset !== "number" || !Number.isInteger(inset) || inset < 0) {
|
|
1763
|
-
throw new AssetMetadataError(`${asset}: nineGrid ${edge} is ${String(inset)}, which is not a whole number of pixels at or above zero`);
|
|
1757
|
+
} else if (char === "]" && depth === 0) {
|
|
1758
|
+
break;
|
|
1764
1759
|
}
|
|
1765
|
-
insets[edge] = inset;
|
|
1766
1760
|
}
|
|
1767
|
-
return
|
|
1761
|
+
return sources;
|
|
1768
1762
|
}
|
|
1769
|
-
function
|
|
1770
|
-
if (value ===
|
|
1771
|
-
|
|
1772
|
-
|
|
1773
|
-
}
|
|
1774
|
-
return value;
|
|
1763
|
+
function isTile(value) {
|
|
1764
|
+
if (typeof value !== "object" || value === null) return false;
|
|
1765
|
+
const record = value;
|
|
1766
|
+
return typeof record.x === "number" && typeof record.y === "number" && typeof record.z === "number" && Array.isArray(record.layers);
|
|
1775
1767
|
}
|
|
1776
|
-
function
|
|
1777
|
-
|
|
1778
|
-
|
|
1779
|
-
|
|
1768
|
+
function parseMapText(text2, path = "the map") {
|
|
1769
|
+
let parsed2;
|
|
1770
|
+
try {
|
|
1771
|
+
parsed2 = JSON.parse(text2);
|
|
1772
|
+
} catch (error) {
|
|
1773
|
+
throw new MapTextError(`${path} is not valid JSON: ${error.message}`);
|
|
1780
1774
|
}
|
|
1781
|
-
|
|
1782
|
-
|
|
1783
|
-
const cells = value[side];
|
|
1784
|
-
if (typeof cells !== "number" || !Number.isInteger(cells)) {
|
|
1785
|
-
throw new AssetMetadataError(`${asset}: collide ${side} is ${String(cells)}, which is not a whole number of cells`);
|
|
1786
|
-
}
|
|
1787
|
-
box[side] = cells;
|
|
1775
|
+
if (typeof parsed2 !== "object" || parsed2 === null || !Array.isArray(parsed2.tiles)) {
|
|
1776
|
+
throw new MapTextError(`${path} holds no tiles array`);
|
|
1788
1777
|
}
|
|
1789
|
-
|
|
1790
|
-
|
|
1778
|
+
const document = parsed2;
|
|
1779
|
+
const sources = scanTileSources(text2);
|
|
1780
|
+
if (sources.length !== document.tiles.length) {
|
|
1781
|
+
throw new MapTextError(`${path}: the scanner found ${sources.length} tile sources for ${document.tiles.length} tiles`);
|
|
1791
1782
|
}
|
|
1792
|
-
|
|
1783
|
+
const byKey = /* @__PURE__ */ new Map();
|
|
1784
|
+
document.tiles.forEach((tile, index) => {
|
|
1785
|
+
if (!isTile(tile)) throw new MapTextError(`${path}: tile ${index} lacks x, y, z or layers`);
|
|
1786
|
+
byKey.set(tileKey(tile.x, tile.y, tile.z), { tile, source: sources[index] });
|
|
1787
|
+
});
|
|
1788
|
+
return { document, sources: byKey };
|
|
1793
1789
|
}
|
|
1794
|
-
const
|
|
1795
|
-
|
|
1796
|
-
const
|
|
1797
|
-
|
|
1798
|
-
|
|
1799
|
-
if (!isRecord$2(value) || typeof value.x !== "number" || typeof value.y !== "number" || !Number.isFinite(value.x) || !Number.isFinite(value.y)) {
|
|
1800
|
-
throw new AssetMetadataError(`${asset}: offset is not an x and a y in pixels`);
|
|
1790
|
+
const TILE_KEYS = ["x", "y", "z", "layers", "blocking", "zone", "audioZone", "interact"];
|
|
1791
|
+
function serializeTile(tile) {
|
|
1792
|
+
const ordered = {};
|
|
1793
|
+
for (const key of TILE_KEYS) {
|
|
1794
|
+
if (tile[key] !== void 0) ordered[key] = tile[key];
|
|
1801
1795
|
}
|
|
1802
|
-
for (const
|
|
1803
|
-
|
|
1804
|
-
if (Math.abs(px) > MAX_DRAW_OFFSET) {
|
|
1805
|
-
throw new AssetMetadataError(`${asset}: offset ${axis} is ${px} px, and an offset is within ${MAX_DRAW_OFFSET} px of the tile`);
|
|
1806
|
-
}
|
|
1796
|
+
for (const key of Object.keys(tile)) {
|
|
1797
|
+
if (!(key in ordered) && tile[key] !== void 0) ordered[key] = tile[key];
|
|
1807
1798
|
}
|
|
1808
|
-
return
|
|
1799
|
+
return JSON.stringify(ordered);
|
|
1809
1800
|
}
|
|
1810
|
-
function
|
|
1811
|
-
|
|
1812
|
-
|
|
1813
|
-
|
|
1801
|
+
function formatMap(next, previous) {
|
|
1802
|
+
const lines = [];
|
|
1803
|
+
let changed = 0;
|
|
1804
|
+
for (const tile of next.tiles) {
|
|
1805
|
+
{
|
|
1806
|
+
lines.push(serializeTile(tile));
|
|
1807
|
+
changed += 1;
|
|
1808
|
+
}
|
|
1814
1809
|
}
|
|
1815
|
-
|
|
1816
|
-
|
|
1810
|
+
const rest = {};
|
|
1811
|
+
for (const key of Object.keys(next)) {
|
|
1812
|
+
if (key !== "width" && key !== "height" && key !== "tiles") rest[key] = next[key];
|
|
1817
1813
|
}
|
|
1818
|
-
|
|
1814
|
+
const trailer = Object.keys(rest).length === 0 ? "" : `,${JSON.stringify(rest).slice(1, -1)}`;
|
|
1815
|
+
const head = `{"width":${JSON.stringify(next.width)},"height":${JSON.stringify(next.height)},"tiles":[`;
|
|
1816
|
+
const body = lines.length === 0 ? "" : `
|
|
1817
|
+
${lines.join(",\n")}
|
|
1818
|
+
`;
|
|
1819
|
+
return { text: `${head}${body}]${trailer}}
|
|
1820
|
+
`, changed };
|
|
1819
1821
|
}
|
|
1820
|
-
|
|
1821
|
-
|
|
1822
|
-
|
|
1823
|
-
}
|
|
1824
|
-
const kind = value.kind;
|
|
1825
|
-
if (typeof kind !== "string") {
|
|
1826
|
-
throw new AssetMetadataError(`${asset}: declares no kind`);
|
|
1827
|
-
}
|
|
1828
|
-
if (!ASSET_KINDS.includes(kind)) {
|
|
1829
|
-
throw new AssetMetadataError(`${asset}: unknown kind ${kind} (expected ${ASSET_KINDS.join(", ")})`);
|
|
1830
|
-
}
|
|
1831
|
-
const minimap = value.minimap;
|
|
1832
|
-
if (minimap !== void 0 && (typeof minimap !== "number" || !Number.isInteger(minimap))) {
|
|
1833
|
-
throw new AssetMetadataError(`${asset}: minimap category is not a whole number`);
|
|
1834
|
-
}
|
|
1835
|
-
if (kind !== "ui" && (value.nineGrid !== void 0 || value.scale !== void 0)) {
|
|
1836
|
-
throw new AssetMetadataError(`${asset}: nineGrid and scale are interface fields, and this is a ${kind}`);
|
|
1837
|
-
}
|
|
1838
|
-
if (kind !== "tile" && (value.collide !== void 0 || value.offset !== void 0 || value.drawScale !== void 0)) {
|
|
1839
|
-
throw new AssetMetadataError(`${asset}: collide, offset and drawScale are tile fields, and this is a ${kind}`);
|
|
1840
|
-
}
|
|
1841
|
-
const animations = readAnimations(value.animations, asset, kind);
|
|
1842
|
-
return {
|
|
1843
|
-
kind,
|
|
1844
|
-
animations,
|
|
1845
|
-
plays: readPlays(value.plays, asset, animations, kind),
|
|
1846
|
-
blocking: value.blocking === true,
|
|
1847
|
-
foreground: value.foreground === true,
|
|
1848
|
-
roof: value.roof === true,
|
|
1849
|
-
door: value.door === true,
|
|
1850
|
-
water: value.water === true,
|
|
1851
|
-
minimap: minimap ?? 0,
|
|
1852
|
-
collide: readCollide(value.collide, asset),
|
|
1853
|
-
offset: readOffset(value.offset, asset),
|
|
1854
|
-
drawScale: readDrawScale(value.drawScale, asset),
|
|
1855
|
-
nineGrid: readNineGrid(value.nineGrid, asset),
|
|
1856
|
-
scale: readScale(value.scale, asset)
|
|
1857
|
-
};
|
|
1822
|
+
const ROUTE_MULTIPLE = 16;
|
|
1823
|
+
function blank(width, height) {
|
|
1824
|
+
return { width, height, data: new Uint8ClampedArray(width * height * 4) };
|
|
1858
1825
|
}
|
|
1859
|
-
|
|
1860
|
-
|
|
1861
|
-
|
|
1862
|
-
|
|
1863
|
-
|
|
1864
|
-
|
|
1865
|
-
|
|
1866
|
-
|
|
1867
|
-
|
|
1826
|
+
const CELLS_PER_TILE = 64;
|
|
1827
|
+
const MAX_CELL = 16;
|
|
1828
|
+
const MIN_CELL = 8;
|
|
1829
|
+
const ROUTE_MAX_EDGE = 3840;
|
|
1830
|
+
const ROUTE_MAX_PIXELS = 8294400;
|
|
1831
|
+
const ROUTE_MIN_PIXELS = 655360;
|
|
1832
|
+
const ROUTE_STEP = 16;
|
|
1833
|
+
function gridForCells(cols, rows, cellsPerTile = CELLS_PER_TILE) {
|
|
1834
|
+
if (!Number.isInteger(cols) || !Number.isInteger(rows) || cols <= 0 || rows <= 0) return null;
|
|
1835
|
+
const drawn = (cell) => cols * cell <= ROUTE_MAX_EDGE && rows * cell <= ROUTE_MAX_EDGE && cols * cell * rows * cell <= ROUTE_MAX_PIXELS && cols * cell % ROUTE_STEP === 0 && rows * cell % ROUTE_STEP === 0;
|
|
1836
|
+
const unscaled = (cell) => cols * cell * rows * cell >= ROUTE_MIN_PIXELS;
|
|
1837
|
+
const canvas = (cell) => ({ width: cols * cell, height: rows * cell, cell, pxPerTile: cellsPerTile * cell });
|
|
1838
|
+
if (drawn(MAX_CELL) && unscaled(MAX_CELL)) return canvas(MAX_CELL);
|
|
1839
|
+
if (drawn(MAX_CELL)) {
|
|
1840
|
+
for (let cell = MAX_CELL + 1; cell * Math.max(cols, rows) <= ROUTE_MAX_EDGE; cell += 1) if (drawn(cell) && unscaled(cell)) return canvas(cell);
|
|
1841
|
+
return null;
|
|
1868
1842
|
}
|
|
1869
|
-
|
|
1870
|
-
|
|
1871
|
-
|
|
1843
|
+
for (let cell = MAX_CELL - 1; cell >= MIN_CELL; cell -= 1) if (drawn(cell) && unscaled(cell)) return canvas(cell);
|
|
1844
|
+
return null;
|
|
1845
|
+
}
|
|
1846
|
+
const LEAST_CELLS_PER_TILE = 16;
|
|
1847
|
+
function cellCounts(cellsPerTile) {
|
|
1848
|
+
const counts = [cellsPerTile];
|
|
1849
|
+
for (let cells = cellsPerTile / 2; Number.isInteger(cells) && cells >= LEAST_CELLS_PER_TILE; cells /= 2) counts.push(cells);
|
|
1850
|
+
return counts;
|
|
1851
|
+
}
|
|
1852
|
+
function gridCanvas(footprint, cellsPerTile = CELLS_PER_TILE) {
|
|
1853
|
+
for (const cells of cellCounts(cellsPerTile)) {
|
|
1854
|
+
const canvas = gridForCells(footprint.cols * cells, footprint.rows * cells, cells);
|
|
1855
|
+
if (canvas !== null) return canvas;
|
|
1872
1856
|
}
|
|
1873
|
-
|
|
1874
|
-
|
|
1875
|
-
|
|
1857
|
+
return null;
|
|
1858
|
+
}
|
|
1859
|
+
function gridProblem(footprint, cellsPerTile = CELLS_PER_TILE) {
|
|
1860
|
+
if (gridCanvas(footprint, cellsPerTile) !== null) return null;
|
|
1861
|
+
const least = cellCounts(cellsPerTile).at(-1);
|
|
1862
|
+
const side = Math.floor(ROUTE_MAX_EDGE / (least * MIN_CELL));
|
|
1863
|
+
const area = Math.floor(ROUTE_MAX_PIXELS / (least * MIN_CELL) ** 2);
|
|
1864
|
+
return `a ${footprint.cols}×${footprint.rows} tile object is too large to draw on the grid: at most ${side} tile${side === 1 ? "" : "s"} on a side and ${area} tile${area === 1 ? "" : "s"} in all`;
|
|
1865
|
+
}
|
|
1866
|
+
const WINDOW_PX = { width: 1536, height: 1024 };
|
|
1867
|
+
const SCALES = [TILE_SIZE, TILE_SIZE / 2, TILE_SIZE / 4, TILE_SIZE / 8, TILE_SIZE / 16];
|
|
1868
|
+
function scaleFor(tilesWide, tilesHigh) {
|
|
1869
|
+
if (tilesWide <= 0 || tilesHigh <= 0) return null;
|
|
1870
|
+
for (const scale of SCALES) {
|
|
1871
|
+
if (tilesWide * scale <= WINDOW_PX.width && tilesHigh * scale <= WINDOW_PX.height) return scale;
|
|
1876
1872
|
}
|
|
1877
|
-
|
|
1878
|
-
|
|
1879
|
-
|
|
1873
|
+
return null;
|
|
1874
|
+
}
|
|
1875
|
+
({
|
|
1876
|
+
width: Math.floor(WINDOW_PX.width / SCALES.at(-1)),
|
|
1877
|
+
height: Math.floor(WINDOW_PX.height / SCALES.at(-1))
|
|
1878
|
+
});
|
|
1879
|
+
function designCanvas(tiles) {
|
|
1880
|
+
if (!Number.isInteger(tiles.cols) || !Number.isInteger(tiles.rows) || tiles.cols < 1 || tiles.rows < 1) return null;
|
|
1881
|
+
const scale = scaleFor(tiles.cols, tiles.rows);
|
|
1882
|
+
if (scale === null) return null;
|
|
1883
|
+
return { cols: tiles.cols, rows: tiles.rows, scale, width: tiles.cols * scale, height: tiles.rows * scale };
|
|
1884
|
+
}
|
|
1885
|
+
const CONCEPT_ART = "concept-art";
|
|
1886
|
+
const OLD_CONCEPT_ART = "level-design";
|
|
1887
|
+
function currentKind(kind) {
|
|
1888
|
+
return kind === OLD_CONCEPT_ART ? CONCEPT_ART : kind;
|
|
1889
|
+
}
|
|
1890
|
+
const DEFAULT_CONCEPT_SIZE = { width: 1536, height: 1024 };
|
|
1891
|
+
const DEFAULT_CONCEPT_TILES = { cols: 6, rows: 4 };
|
|
1892
|
+
const CONCEPT_SIDE = { min: 256, max: 1536 };
|
|
1893
|
+
const WINDOWS = [
|
|
1894
|
+
{ width: 1536, height: 1024 },
|
|
1895
|
+
{ width: 1024, height: 1536 }
|
|
1896
|
+
];
|
|
1897
|
+
function conceptSizeProblem(size) {
|
|
1898
|
+
const { width, height } = size;
|
|
1899
|
+
const side = (value) => Number.isInteger(value) && value >= CONCEPT_SIDE.min && value <= CONCEPT_SIDE.max && value % ROUTE_MULTIPLE === 0;
|
|
1900
|
+
if (!side(width) || !side(height))
|
|
1901
|
+
return `a concept art's sides are multiples of ${ROUTE_MULTIPLE} from ${CONCEPT_SIDE.min} to ${CONCEPT_SIDE.max} px, not ${width} × ${height}`;
|
|
1902
|
+
if (!WINDOWS.some((window) => width <= window.width && height <= window.height))
|
|
1903
|
+
return `a ${width} × ${height} px concept art is past the route's window: it fits within 1536 × 1024 or 1024 × 1536`;
|
|
1904
|
+
return null;
|
|
1905
|
+
}
|
|
1906
|
+
function conceptShapeProblem(shape) {
|
|
1907
|
+
if (shape.pixelArt === true) {
|
|
1908
|
+
if (shape.size !== void 0) return "a pixel-art concept art is sized in tiles, not in pixels";
|
|
1909
|
+
if (shape.background === "transparent") return "a pixel-art concept art is a whole area, with no background to cut away";
|
|
1910
|
+
const tiles = shape.tiles ?? DEFAULT_CONCEPT_TILES;
|
|
1911
|
+
return designCanvas(tiles) === null ? `a ${tiles.cols}×${tiles.rows} tile concept art does not fit the route's window` : null;
|
|
1880
1912
|
}
|
|
1881
|
-
return
|
|
1913
|
+
if (shape.tiles !== void 0) return "tiles are a pixel-art concept art's: ask for pixel art too";
|
|
1914
|
+
return shape.size === void 0 ? null : conceptSizeProblem(shape.size);
|
|
1882
1915
|
}
|
|
1883
|
-
|
|
1884
|
-
|
|
1885
|
-
|
|
1886
|
-
|
|
1887
|
-
|
|
1888
|
-
|
|
1889
|
-
|
|
1890
|
-
|
|
1891
|
-
|
|
1892
|
-
|
|
1893
|
-
|
|
1894
|
-
|
|
1895
|
-
if (
|
|
1896
|
-
if (entry.nineGrid !== void 0) out.nineGrid = entry.nineGrid;
|
|
1897
|
-
if (entry.scale !== void 0) out.scale = entry.scale;
|
|
1898
|
-
if (entry.animations.size > 0) {
|
|
1899
|
-
const animations = {};
|
|
1900
|
-
const ordered = [...entry.animations].sort(([a], [b]) => a < b ? -1 : a > b ? 1 : 0);
|
|
1901
|
-
for (const [animation, frames] of ordered) {
|
|
1902
|
-
animations[animation] = frames.map((frame) => frame.flip ? { asset: frame.asset, ms: frame.ms, flip: true } : { asset: frame.asset, ms: frame.ms });
|
|
1903
|
-
}
|
|
1904
|
-
out.animations = animations;
|
|
1905
|
-
}
|
|
1906
|
-
if (entry.plays.size > 0) {
|
|
1907
|
-
const plays = {};
|
|
1908
|
-
for (const [animation, play] of [...entry.plays].sort(([a], [b]) => a < b ? -1 : a > b ? 1 : 0)) plays[animation] = play;
|
|
1909
|
-
out.plays = plays;
|
|
1910
|
-
}
|
|
1911
|
-
assets[name] = out;
|
|
1916
|
+
const MAX_ART_TAGS = 8;
|
|
1917
|
+
const TAG = /^[a-z0-9][a-z0-9-]{0,31}$/;
|
|
1918
|
+
function tagProblem(tag2) {
|
|
1919
|
+
return TAG.test(tag2) ? null : `${JSON.stringify(tag2)} is not a tag: 1 to 32 lowercase letters, digits and dashes, starting with a letter or a digit`;
|
|
1920
|
+
}
|
|
1921
|
+
function tagOf(typed) {
|
|
1922
|
+
return typed.trim().toLowerCase().replace(/[\s_]+/g, "-");
|
|
1923
|
+
}
|
|
1924
|
+
function tagsProblem(tags) {
|
|
1925
|
+
if (tags.length > MAX_ART_TAGS) return `a concept art carries at most ${MAX_ART_TAGS} tags, not ${tags.length}`;
|
|
1926
|
+
for (const tag2 of tags) {
|
|
1927
|
+
const problem = tagProblem(tag2);
|
|
1928
|
+
if (problem !== null) return problem;
|
|
1912
1929
|
}
|
|
1913
|
-
|
|
1914
|
-
`;
|
|
1930
|
+
const repeated = tags.find((tag2, at) => tags.indexOf(tag2) !== at);
|
|
1931
|
+
return repeated === void 0 ? null : `${repeated} is given twice`;
|
|
1915
1932
|
}
|
|
1916
|
-
|
|
1917
|
-
|
|
1918
|
-
|
|
1933
|
+
const SECTOR_SIDE$1 = 128;
|
|
1934
|
+
const SECTOR_CELLS = SECTOR_SIDE$1 * SECTOR_SIDE$1;
|
|
1935
|
+
const SECTOR_BYTES = 512 * 1024;
|
|
1936
|
+
const MAP_LEVELS = { first: 0, last: 14 };
|
|
1937
|
+
const LARGEST_MAP_SIDE = 1e4;
|
|
1938
|
+
const OUTLINE_ACROSS = Math.ceil(LARGEST_MAP_SIDE / SECTOR_SIDE$1);
|
|
1939
|
+
const SECTOR_FORMAT = 1;
|
|
1940
|
+
const MAGIC = [80, 86, 67];
|
|
1941
|
+
const PRESENT = 1;
|
|
1942
|
+
const AUDIO = 2;
|
|
1943
|
+
const INTERACT = 4;
|
|
1944
|
+
const EXTRA = 8;
|
|
1945
|
+
const U16 = 65535;
|
|
1946
|
+
const MAX_LAYERS = 255;
|
|
1947
|
+
const NAMED$1 = /* @__PURE__ */ new Set(["x", "y", "z", "layers", "blocking", "zone", "audioZone", "interact"]);
|
|
1948
|
+
class SectorError extends Error {
|
|
1919
1949
|
constructor(message) {
|
|
1920
1950
|
super(message);
|
|
1921
|
-
this.name = "
|
|
1951
|
+
this.name = "SectorError";
|
|
1922
1952
|
}
|
|
1923
1953
|
}
|
|
1924
|
-
|
|
1925
|
-
|
|
1926
|
-
|
|
1927
|
-
|
|
1928
|
-
|
|
1929
|
-
|
|
1930
|
-
|
|
1931
|
-
|
|
1954
|
+
function u16(value, what) {
|
|
1955
|
+
if (!Number.isInteger(value) || value < 0 || value > U16) throw new SectorError(`${what} is ${value}, and a sector holds a whole number from 0 to 65,535`);
|
|
1956
|
+
return value;
|
|
1957
|
+
}
|
|
1958
|
+
function encodeSector(cells) {
|
|
1959
|
+
if (cells.length !== SECTOR_CELLS) throw new SectorError(`a sector is ${SECTOR_CELLS} cells, not ${cells.length}`);
|
|
1960
|
+
const encoder2 = new TextEncoder();
|
|
1961
|
+
const bytes = [...MAGIC, SECTOR_FORMAT];
|
|
1962
|
+
const push16 = (value) => bytes.push(value & 255, value >> 8);
|
|
1963
|
+
for (const cell of cells) {
|
|
1964
|
+
if (cell === void 0) {
|
|
1965
|
+
bytes.push(0);
|
|
1966
|
+
continue;
|
|
1932
1967
|
}
|
|
1933
|
-
|
|
1934
|
-
|
|
1935
|
-
|
|
1936
|
-
|
|
1937
|
-
|
|
1938
|
-
|
|
1939
|
-
|
|
1968
|
+
const extra = cell.extra === void 0 ? null : encoder2.encode(JSON.stringify(cell.extra));
|
|
1969
|
+
if (extra !== null && extra.length > U16) throw new SectorError("a tile's extra fields are past 65,535 bytes");
|
|
1970
|
+
if (cell.layers.length > MAX_LAYERS) throw new SectorError(`a tile has ${cell.layers.length} layers, and a sector holds at most ${MAX_LAYERS}`);
|
|
1971
|
+
bytes.push(PRESENT | (cell.audioZone === void 0 ? 0 : AUDIO) | (cell.interact === void 0 ? 0 : INTERACT) | (extra === null ? 0 : EXTRA));
|
|
1972
|
+
bytes.push(cell.layers.length);
|
|
1973
|
+
for (const layer of cell.layers) push16(u16(layer, "a layer id"));
|
|
1974
|
+
push16(u16(cell.blocking, "blocking"));
|
|
1975
|
+
push16(u16(cell.zone, "a zone"));
|
|
1976
|
+
if (cell.audioZone !== void 0) push16(u16(cell.audioZone, "an audio zone"));
|
|
1977
|
+
if (cell.interact !== void 0) push16(u16(cell.interact, "interact"));
|
|
1978
|
+
if (extra !== null) {
|
|
1979
|
+
push16(extra.length);
|
|
1980
|
+
for (const byte of extra) bytes.push(byte);
|
|
1940
1981
|
}
|
|
1941
|
-
const name = assetName(value, `looktype ${key}`);
|
|
1942
|
-
looktypes.set(Number(key), name);
|
|
1943
|
-
names.add(name);
|
|
1944
1982
|
}
|
|
1945
|
-
|
|
1946
|
-
|
|
1947
|
-
|
|
1983
|
+
return Uint8Array.from(bytes);
|
|
1984
|
+
}
|
|
1985
|
+
function decodeSector(bytes) {
|
|
1986
|
+
if (bytes.length < 4 || MAGIC.some((byte, index) => bytes[index] !== byte)) throw new SectorError("these bytes are not a map sector");
|
|
1987
|
+
if (bytes[3] !== SECTOR_FORMAT) throw new SectorError(`a map sector of format ${bytes[3]} is not one this version reads`);
|
|
1988
|
+
const decoder = new TextDecoder("utf-8", { fatal: true });
|
|
1989
|
+
let at = 4;
|
|
1990
|
+
const need = (count) => {
|
|
1991
|
+
if (at + count > bytes.length) throw new SectorError("the sector's bytes end before its last cell");
|
|
1992
|
+
};
|
|
1993
|
+
const read8 = () => {
|
|
1994
|
+
need(1);
|
|
1995
|
+
return bytes[at++];
|
|
1996
|
+
};
|
|
1997
|
+
const read16 = () => {
|
|
1998
|
+
need(2);
|
|
1999
|
+
const value = bytes[at] | bytes[at + 1] << 8;
|
|
2000
|
+
at += 2;
|
|
2001
|
+
return value;
|
|
2002
|
+
};
|
|
2003
|
+
const cells = [];
|
|
2004
|
+
for (let index = 0; index < SECTOR_CELLS; index += 1) {
|
|
2005
|
+
const flags = read8();
|
|
2006
|
+
if ((flags & PRESENT) === 0) {
|
|
2007
|
+
cells.push(void 0);
|
|
2008
|
+
continue;
|
|
1948
2009
|
}
|
|
1949
|
-
const
|
|
1950
|
-
|
|
1951
|
-
|
|
2010
|
+
const count = read8();
|
|
2011
|
+
const layers = [];
|
|
2012
|
+
for (let layer = 0; layer < count; layer += 1) layers.push(read16());
|
|
2013
|
+
const blocking = read16();
|
|
2014
|
+
const zone = read16();
|
|
2015
|
+
const audioZone = (flags & AUDIO) === 0 ? void 0 : read16();
|
|
2016
|
+
const interact = (flags & INTERACT) === 0 ? void 0 : read16();
|
|
2017
|
+
let extra;
|
|
2018
|
+
if ((flags & EXTRA) !== 0) {
|
|
2019
|
+
const length = read16();
|
|
2020
|
+
need(length);
|
|
2021
|
+
let parsed2;
|
|
2022
|
+
try {
|
|
2023
|
+
parsed2 = JSON.parse(decoder.decode(bytes.subarray(at, at + length)));
|
|
2024
|
+
} catch {
|
|
2025
|
+
throw new SectorError("a tile's extra fields are not JSON");
|
|
2026
|
+
}
|
|
2027
|
+
if (typeof parsed2 !== "object" || parsed2 === null || Array.isArray(parsed2)) throw new SectorError("a tile's extra fields are not an object");
|
|
2028
|
+
const named = Object.keys(parsed2).find((key) => NAMED$1.has(key));
|
|
2029
|
+
if (named !== void 0) throw new SectorError(`a tile's extra fields name ${named}, which the sector holds itself`);
|
|
2030
|
+
extra = parsed2;
|
|
2031
|
+
at += length;
|
|
2032
|
+
}
|
|
2033
|
+
cells.push({
|
|
2034
|
+
layers,
|
|
2035
|
+
blocking,
|
|
2036
|
+
zone,
|
|
2037
|
+
...audioZone === void 0 ? {} : { audioZone },
|
|
2038
|
+
...interact === void 0 ? {} : { interact },
|
|
2039
|
+
...extra === void 0 ? {} : { extra }
|
|
2040
|
+
});
|
|
1952
2041
|
}
|
|
2042
|
+
if (at !== bytes.length) throw new SectorError("the sector's bytes go on past its last cell");
|
|
2043
|
+
return cells;
|
|
2044
|
+
}
|
|
2045
|
+
const NAMED = /* @__PURE__ */ new Set(["x", "y", "z", "layers", "blocking", "zone", "audioZone", "interact"]);
|
|
2046
|
+
function cellOfTile(tile) {
|
|
2047
|
+
const extra = Object.fromEntries(Object.entries(tile).filter(([key]) => !NAMED.has(key)));
|
|
1953
2048
|
return {
|
|
1954
|
-
|
|
1955
|
-
|
|
1956
|
-
|
|
1957
|
-
|
|
1958
|
-
|
|
2049
|
+
layers: tile.layers,
|
|
2050
|
+
blocking: tile.blocking,
|
|
2051
|
+
zone: tile.zone,
|
|
2052
|
+
...tile.audioZone === void 0 ? {} : { audioZone: tile.audioZone },
|
|
2053
|
+
...tile.interact === void 0 ? {} : { interact: tile.interact },
|
|
2054
|
+
...Object.keys(extra).length === 0 ? {} : { extra }
|
|
1959
2055
|
};
|
|
1960
2056
|
}
|
|
1961
|
-
function
|
|
1962
|
-
|
|
1963
|
-
|
|
1964
|
-
|
|
1965
|
-
|
|
1966
|
-
|
|
1967
|
-
|
|
1968
|
-
|
|
1969
|
-
|
|
1970
|
-
|
|
1971
|
-
|
|
1972
|
-
}
|
|
1973
|
-
|
|
1974
|
-
|
|
1975
|
-
|
|
1976
|
-
|
|
1977
|
-
|
|
1978
|
-
if (typeof value !== "string" || value.trim() === "") {
|
|
1979
|
-
throw new IdMapError(`${what}: the asset name must be a non-empty string`);
|
|
1980
|
-
}
|
|
1981
|
-
return value;
|
|
1982
|
-
}
|
|
1983
|
-
const EAC_MAGIC = 826491205;
|
|
1984
|
-
const EAC_VERSION = 1;
|
|
1985
|
-
const INDEX_ENTRY = "assets.eai";
|
|
1986
|
-
const ID_MAP_ENTRY = "id-map.json";
|
|
1987
|
-
const HEADER_SIZE = 32;
|
|
1988
|
-
const FLAG_MASKED = 1;
|
|
1989
|
-
const MASK_KEY = Uint8Array.from([69, 116, 101, 114, 110, 105, 116, 121, 77, 69, 45, 115, 101, 97, 108, 33]);
|
|
1990
|
-
class ContainerError extends Error {
|
|
2057
|
+
function tileOfCell(cell, x, y, z) {
|
|
2058
|
+
return {
|
|
2059
|
+
...cell.extra ?? {},
|
|
2060
|
+
x,
|
|
2061
|
+
y,
|
|
2062
|
+
z,
|
|
2063
|
+
layers: [...cell.layers],
|
|
2064
|
+
blocking: cell.blocking,
|
|
2065
|
+
zone: cell.zone,
|
|
2066
|
+
...cell.audioZone === void 0 ? {} : { audioZone: cell.audioZone },
|
|
2067
|
+
...cell.interact === void 0 ? {} : { interact: cell.interact }
|
|
2068
|
+
};
|
|
2069
|
+
}
|
|
2070
|
+
const MIRROR_MAP_DIRECTORY = "map";
|
|
2071
|
+
const MIRROR_INDEX_FILE = `${MIRROR_MAP_DIRECTORY}/index.json`;
|
|
2072
|
+
const MIRROR_FORMAT = 1;
|
|
2073
|
+
class MirrorError extends Error {
|
|
1991
2074
|
// Assigned in the body: Node's strip-only TypeScript mode rejects parameter
|
|
1992
2075
|
// properties (docs/notes.md n-0023).
|
|
1993
2076
|
constructor(message) {
|
|
1994
2077
|
super(message);
|
|
1995
|
-
this.name = "
|
|
2078
|
+
this.name = "MirrorError";
|
|
1996
2079
|
}
|
|
1997
2080
|
}
|
|
1998
|
-
function
|
|
1999
|
-
|
|
2000
|
-
|
|
2001
|
-
return (value ^ key ^ drift) & 255;
|
|
2081
|
+
function mirrorSectorPath(z, cx, cy) {
|
|
2082
|
+
if (!isPlace(z, cx, cy)) throw new MirrorError(`no sector of a map is at level ${z}, column ${cx}, row ${cy}`);
|
|
2083
|
+
return `${MIRROR_MAP_DIRECTORY}/${z}/${cx}-${cy}.bin`;
|
|
2002
2084
|
}
|
|
2003
|
-
function
|
|
2004
|
-
|
|
2005
|
-
|
|
2006
|
-
}
|
|
2085
|
+
function isPlace(z, cx, cy) {
|
|
2086
|
+
const within = (value, last) => Number.isInteger(value) && value >= 0 && value <= last;
|
|
2087
|
+
return within(z, MAP_LEVELS.last) && z >= MAP_LEVELS.first && within(cx, OUTLINE_ACROSS - 1) && within(cy, OUTLINE_ACROSS - 1);
|
|
2007
2088
|
}
|
|
2008
|
-
function
|
|
2089
|
+
function isSize(value) {
|
|
2090
|
+
const side = (n) => Number.isInteger(n) && n >= 1 && n <= LARGEST_MAP_SIDE;
|
|
2091
|
+
return typeof value === "object" && value !== null && side(value.width) && side(value.height);
|
|
2092
|
+
}
|
|
2093
|
+
function mirrorIndexText(index) {
|
|
2094
|
+
return `${JSON.stringify(index, null, 2)}
|
|
2095
|
+
`;
|
|
2096
|
+
}
|
|
2097
|
+
function parseMirrorIndex(text2) {
|
|
2098
|
+
let value;
|
|
2099
|
+
try {
|
|
2100
|
+
value = JSON.parse(text2);
|
|
2101
|
+
} catch {
|
|
2102
|
+
throw new MirrorError(`${MIRROR_INDEX_FILE} is not JSON`);
|
|
2103
|
+
}
|
|
2104
|
+
const index = value;
|
|
2105
|
+
if (typeof index !== "object" || index === null) throw new MirrorError(`${MIRROR_INDEX_FILE} is not an object`);
|
|
2106
|
+
if (index.format !== MIRROR_FORMAT)
|
|
2107
|
+
throw new MirrorError(`${MIRROR_INDEX_FILE} is in format ${String(index.format)}, and this reads format ${MIRROR_FORMAT}`);
|
|
2108
|
+
if (!Number.isInteger(index.revision) || index.revision < 0) throw new MirrorError(`${MIRROR_INDEX_FILE} names no revision`);
|
|
2109
|
+
if (!isSize(index.size) || !isSize(index.limit)) throw new MirrorError(`${MIRROR_INDEX_FILE} names no size or limit a map may have`);
|
|
2110
|
+
if (!Array.isArray(index.names) || !Array.isArray(index.sectors)) throw new MirrorError(`${MIRROR_INDEX_FILE} lists no levels or sectors`);
|
|
2111
|
+
for (const named of index.names) {
|
|
2112
|
+
if (typeof named?.name !== "string" || !isPlace(named.z, 0, 0)) throw new MirrorError(`${MIRROR_INDEX_FILE} names a level that is none`);
|
|
2113
|
+
}
|
|
2009
2114
|
const seen = /* @__PURE__ */ new Set();
|
|
2010
|
-
for (const
|
|
2011
|
-
|
|
2012
|
-
|
|
2115
|
+
for (const sector of index.sectors) {
|
|
2116
|
+
const [z, cx, cy, version2] = Array.isArray(sector) ? sector : [];
|
|
2117
|
+
if (!isPlace(z, cx, cy) || !Number.isInteger(version2) || version2 < 1) {
|
|
2118
|
+
throw new MirrorError(`${MIRROR_INDEX_FILE} lists a sector that is none: ${JSON.stringify(sector)}`);
|
|
2119
|
+
}
|
|
2120
|
+
const key = `${z}:${cx}:${cy}`;
|
|
2121
|
+
if (cx * SECTOR_SIDE$1 >= index.size.width || cy * SECTOR_SIDE$1 >= index.size.height) {
|
|
2122
|
+
throw new MirrorError(`${MIRROR_INDEX_FILE} lists the sector ${key}, outside the map's size`);
|
|
2123
|
+
}
|
|
2124
|
+
if (seen.has(key)) throw new MirrorError(`${MIRROR_INDEX_FILE} lists the sector ${key} twice`);
|
|
2125
|
+
seen.add(key);
|
|
2013
2126
|
}
|
|
2014
|
-
|
|
2015
|
-
const ordered = [...entries].sort((left, right) => left.name < right.name ? -1 : left.name > right.name ? 1 : 0);
|
|
2016
|
-
const names = ordered.map((entry) => encoder2.encode(entry.name));
|
|
2017
|
-
const tocLength = names.reduce((total, name) => total + 2 + name.length + 8, 0);
|
|
2018
|
-
const tocOffset = HEADER_SIZE;
|
|
2019
|
-
const payloadOffset = tocOffset + tocLength;
|
|
2020
|
-
const payloadLength = ordered.reduce((total, entry) => total + entry.bytes.length, 0);
|
|
2021
|
-
const out = new Uint8Array(payloadOffset + payloadLength);
|
|
2022
|
-
const view = new DataView(out.buffer);
|
|
2023
|
-
view.setUint32(0, EAC_MAGIC, true);
|
|
2024
|
-
view.setUint32(4, EAC_VERSION, true);
|
|
2025
|
-
view.setUint32(8, ordered.length, true);
|
|
2026
|
-
view.setUint32(12, tocOffset, true);
|
|
2027
|
-
view.setUint32(16, tocLength, true);
|
|
2028
|
-
view.setUint32(20, payloadOffset, true);
|
|
2029
|
-
view.setUint32(24, FLAG_MASKED, true);
|
|
2030
|
-
view.setUint32(28, 0, true);
|
|
2031
|
-
const toc = new Uint8Array(tocLength);
|
|
2032
|
-
const tocView = new DataView(toc.buffer);
|
|
2033
|
-
let cursor = 0;
|
|
2034
|
-
let entryOffset = payloadOffset;
|
|
2035
|
-
ordered.forEach((entry, index) => {
|
|
2036
|
-
const name = names[index];
|
|
2037
|
-
tocView.setUint16(cursor, name.length, true);
|
|
2038
|
-
cursor += 2;
|
|
2039
|
-
toc.set(name, cursor);
|
|
2040
|
-
cursor += name.length;
|
|
2041
|
-
tocView.setUint32(cursor, entryOffset, true);
|
|
2042
|
-
cursor += 4;
|
|
2043
|
-
tocView.setUint32(cursor, entry.bytes.length, true);
|
|
2044
|
-
cursor += 4;
|
|
2045
|
-
maskInto(out, entryOffset, entry.bytes);
|
|
2046
|
-
entryOffset += entry.bytes.length;
|
|
2047
|
-
});
|
|
2048
|
-
maskInto(out, tocOffset, toc);
|
|
2049
|
-
return out;
|
|
2050
|
-
}
|
|
2051
|
-
function tileSpritePlacement(tx, ty, width, height, tileSize = TILE_SIZE) {
|
|
2052
|
-
const overhangX = (width - tileSize) / 2;
|
|
2053
|
-
const overhangY = (height - tileSize) / 2;
|
|
2054
|
-
return { x: tx * tileSize - overhangX, y: ty * tileSize - overhangY };
|
|
2127
|
+
return index;
|
|
2055
2128
|
}
|
|
2056
|
-
function
|
|
2057
|
-
const
|
|
2058
|
-
const
|
|
2059
|
-
|
|
2060
|
-
|
|
2061
|
-
|
|
2062
|
-
|
|
2063
|
-
|
|
2064
|
-
|
|
2065
|
-
|
|
2066
|
-
|
|
2067
|
-
|
|
2129
|
+
async function readMirroredMap(index, read, inflate2) {
|
|
2130
|
+
const tiles = [];
|
|
2131
|
+
for (const [z, cx, cy] of index.sectors) {
|
|
2132
|
+
const path = mirrorSectorPath(z, cx, cy);
|
|
2133
|
+
let cells;
|
|
2134
|
+
try {
|
|
2135
|
+
cells = decodeSector(await inflate2(await read(path)));
|
|
2136
|
+
} catch (error) {
|
|
2137
|
+
throw new MirrorError(`${path} is not a sector the map can read: ${error instanceof Error ? error.message : String(error)}`);
|
|
2138
|
+
}
|
|
2139
|
+
for (const [at, cell] of cells.entries()) {
|
|
2140
|
+
if (cell === void 0) continue;
|
|
2141
|
+
const x = cx * SECTOR_SIDE$1 + at % SECTOR_SIDE$1;
|
|
2142
|
+
const y = cy * SECTOR_SIDE$1 + Math.floor(at / SECTOR_SIDE$1);
|
|
2143
|
+
if (x >= index.size.width || y >= index.size.height) continue;
|
|
2144
|
+
tiles.push(tileOfCell(cell, x, y, z));
|
|
2145
|
+
}
|
|
2146
|
+
}
|
|
2147
|
+
tiles.sort((a, b) => a.z - b.z || a.y - b.y || a.x - b.x);
|
|
2148
|
+
return { width: index.size.width, height: index.size.height, tiles };
|
|
2068
2149
|
}
|
|
2069
|
-
|
|
2070
|
-
const
|
|
2071
|
-
|
|
2072
|
-
|
|
2073
|
-
|
|
2074
|
-
|
|
2150
|
+
function sectorsOfMap(map) {
|
|
2151
|
+
const sectors = /* @__PURE__ */ new Map();
|
|
2152
|
+
for (const tile of map.tiles) {
|
|
2153
|
+
const { x, y, z } = tile;
|
|
2154
|
+
if (!Number.isInteger(x) || !Number.isInteger(y) || x < 0 || y < 0 || x >= LARGEST_MAP_SIDE || y >= LARGEST_MAP_SIDE || !isPlace(z, 0, 0)) {
|
|
2155
|
+
throw new MirrorError(`the tile at (${x}, ${y}) on level ${z} is at no place a map has`);
|
|
2156
|
+
}
|
|
2157
|
+
const cx = Math.floor(x / SECTOR_SIDE$1);
|
|
2158
|
+
const cy = Math.floor(y / SECTOR_SIDE$1);
|
|
2159
|
+
const key = `${z}:${cx}:${cy}`;
|
|
2160
|
+
let sector = sectors.get(key);
|
|
2161
|
+
if (sector === void 0) {
|
|
2162
|
+
sector = { z, cx, cy, cells: new Array(SECTOR_CELLS) };
|
|
2163
|
+
sectors.set(key, sector);
|
|
2164
|
+
}
|
|
2165
|
+
sector.cells[y % SECTOR_SIDE$1 * SECTOR_SIDE$1 + x % SECTOR_SIDE$1] = cellOfTile(tile);
|
|
2075
2166
|
}
|
|
2076
|
-
return
|
|
2167
|
+
return [...sectors.values()].sort((a, b) => a.z - b.z || a.cy - b.cy || a.cx - b.cx);
|
|
2077
2168
|
}
|
|
2078
|
-
|
|
2079
|
-
|
|
2080
|
-
|
|
2081
|
-
}
|
|
2169
|
+
const SIGNATURE$1 = [137, 80, 78, 71, 13, 10, 26, 10];
|
|
2170
|
+
function isPng(bytes) {
|
|
2171
|
+
return bytes.length >= 24 && SIGNATURE$1.every((byte, index) => bytes[index] === byte);
|
|
2172
|
+
}
|
|
2173
|
+
function pngSize(bytes) {
|
|
2174
|
+
if (!isPng(bytes)) throw new RangeError("those bytes are not a PNG");
|
|
2175
|
+
const view = new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength);
|
|
2176
|
+
return { width: view.getUint32(16), height: view.getUint32(20) };
|
|
2177
|
+
}
|
|
2178
|
+
const GROUND_KINDS = /* @__PURE__ */ new Set(["ground", "floor", "wall", "cliff", "interior", "grass-stone"]);
|
|
2179
|
+
function isGroundName(name) {
|
|
2180
|
+
const { inner } = splitTileset(name);
|
|
2181
|
+
const kind = inner.split("/")[1] ?? "";
|
|
2182
|
+
return GROUND_KINDS.has(kind) || inner.includes("/wang-");
|
|
2183
|
+
}
|
|
2184
|
+
const MAX_ASSEMBLED_PX = 4096;
|
|
2082
2185
|
const REACHED = Array.from({ length: 16 }, (_value, mask) => [0, 1, 2, 3].filter((corner) => (mask >> corner & 1) === 1));
|
|
2083
2186
|
REACHED.map((reached) => [0, 1, 2, 3].find((corner) => !reached.includes(corner)));
|
|
2084
2187
|
Array.from({ length: 16 }, (_value, index) => index);
|
|
@@ -2702,7 +2805,7 @@ function assetOf(id2, raw) {
|
|
|
2702
2805
|
if (!Array.isArray(files) || files.length === 0 || !files.every((file) => typeof file === "string" && isPulledPath(file))) {
|
|
2703
2806
|
throw new PulledError(`${PULLED_FILE}: the asset ${id2} names a file that is not a picture under out/ or ref/`);
|
|
2704
2807
|
}
|
|
2705
|
-
return { kind, name, hash, files };
|
|
2808
|
+
return { kind: currentKind(kind), name, hash, files };
|
|
2706
2809
|
}
|
|
2707
2810
|
function guideOf(raw) {
|
|
2708
2811
|
if (raw === void 0 || raw === null) return null;
|
|
@@ -2750,7 +2853,7 @@ async function readRecord(local) {
|
|
|
2750
2853
|
async function writeRecord(local, record) {
|
|
2751
2854
|
await artBoundary(local).writeFileReplacing(recordPath(local), writePulled(record));
|
|
2752
2855
|
}
|
|
2753
|
-
const REFERENCE_KINDS = /* @__PURE__ */ new Set([
|
|
2856
|
+
const REFERENCE_KINDS = /* @__PURE__ */ new Set([CONCEPT_ART, "hud-mockup"]);
|
|
2754
2857
|
function setPieces(asset) {
|
|
2755
2858
|
return setPiecesOf(asset.kind, asset.images.length, asset.data);
|
|
2756
2859
|
}
|
|
@@ -2819,23 +2922,28 @@ const date = (at) => at.slice(0, 10);
|
|
|
2819
2922
|
function listed(assets) {
|
|
2820
2923
|
if (assets.length === 0) return "No assets.";
|
|
2821
2924
|
return assets.map(
|
|
2822
|
-
(asset) => `${asset.id} ${asset.kind.padEnd(12)} ${(asset.name ?? "—").padEnd(28)} ${asset.images} image${asset.images === 1 ? "" : "s"} ${date(asset.createdAt)}`
|
|
2925
|
+
(asset) => `${asset.id} ${asset.kind.padEnd(12)} ${(asset.name ?? "—").padEnd(28)} ${asset.images} image${asset.images === 1 ? "" : "s"} ${date(asset.createdAt)}${asset.tags === void 0 || asset.tags.length === 0 ? "" : ` #${asset.tags.join(" #")}`}`
|
|
2823
2926
|
).join("\n");
|
|
2824
2927
|
}
|
|
2825
2928
|
const list = {
|
|
2826
2929
|
name: "list",
|
|
2827
|
-
summary: "List the platform project's assets, of one kind when given, newest first",
|
|
2828
|
-
usage: "[<kind>] [--before <id>]",
|
|
2829
|
-
options: {
|
|
2930
|
+
summary: "List the platform project's assets, of one kind and carrying one tag when given, newest first",
|
|
2931
|
+
usage: "[<kind>] [--tag <tag>] [--before <id>]",
|
|
2932
|
+
options: {
|
|
2933
|
+
tag: { type: "string", value: "<tag>", description: "only the concept art carrying this tag, such as anchor" },
|
|
2934
|
+
before: { type: "string", value: "<id>", description: "the page after the asset with this id, as the last page said" }
|
|
2935
|
+
},
|
|
2830
2936
|
positionals: [0, 1],
|
|
2831
2937
|
run: async (context, args) => {
|
|
2832
2938
|
const local = await projectHere(context.io.cwd);
|
|
2833
2939
|
const project = linkedOf(local);
|
|
2834
2940
|
const platform = await signedIn(context);
|
|
2835
2941
|
const [kind] = args.positionals;
|
|
2836
|
-
const
|
|
2942
|
+
const given = stringOf(args, "tag");
|
|
2943
|
+
const tag2 = given === void 0 ? null : tagOf(given);
|
|
2944
|
+
const page = await platform.web.assets(project, kind ?? null, stringOf(args, "before") ?? null, tag2);
|
|
2837
2945
|
const more = page.next === null ? "" : `
|
|
2838
|
-
More: pixel-vibing-cli list${kind === void 0 ? "" : ` ${kind}`} --before ${page.next}`;
|
|
2946
|
+
More: pixel-vibing-cli list${kind === void 0 ? "" : ` ${kind}`}${tag2 === null ? "" : ` --tag ${tag2}`} --before ${page.next}`;
|
|
2839
2947
|
answer(context, page, `${listed(page.assets)}${more}`);
|
|
2840
2948
|
}
|
|
2841
2949
|
};
|
|
@@ -2856,6 +2964,39 @@ const pull = {
|
|
|
2856
2964
|
answer(context, pulled, pulledWords(pulled));
|
|
2857
2965
|
}
|
|
2858
2966
|
};
|
|
2967
|
+
const tag = {
|
|
2968
|
+
name: "tag",
|
|
2969
|
+
summary: "Say what a concept art is for: set its tags, by its id or its name, or show them when none are given",
|
|
2970
|
+
usage: "<id|name> [<tag>…] [--none]",
|
|
2971
|
+
options: { none: { type: "boolean", description: "take every tag off it" } },
|
|
2972
|
+
positionals: [1, 9],
|
|
2973
|
+
run: async (context, args) => {
|
|
2974
|
+
const local = await projectHere(context.io.cwd);
|
|
2975
|
+
const project = linkedOf(local);
|
|
2976
|
+
const [ref, ...typed] = args.positionals;
|
|
2977
|
+
const none = flagOf(args, "none");
|
|
2978
|
+
if (none && typed.length > 0) throw new CliError("--none takes every tag off: give tags or --none, not both");
|
|
2979
|
+
const platform = await signedIn(context);
|
|
2980
|
+
if (typed.length === 0 && !none) {
|
|
2981
|
+
const asset = await platform.web.asset(project, ref);
|
|
2982
|
+
if (asset.kind !== CONCEPT_ART) throw new CliError(`${ref} is ${asset.kind}: only concept art carries tags`);
|
|
2983
|
+
const tags2 = asset.tags ?? [];
|
|
2984
|
+
const offered = (await platform.web.tags(project)).map((each) => each.name);
|
|
2985
|
+
answer(
|
|
2986
|
+
context,
|
|
2987
|
+
{ id: asset.id, tags: tags2, offered },
|
|
2988
|
+
`${tags2.length === 0 ? `${ref} carries no tag.` : `${ref}: #${tags2.join(" #")}`}
|
|
2989
|
+
The project's tags: ${offered.join(", ")}`
|
|
2990
|
+
);
|
|
2991
|
+
return;
|
|
2992
|
+
}
|
|
2993
|
+
const tags = typed.map(tagOf);
|
|
2994
|
+
const problem = tagsProblem(tags);
|
|
2995
|
+
if (problem !== null) throw new CliError(problem);
|
|
2996
|
+
const set = await platform.web.tagAsset(project, ref, tags);
|
|
2997
|
+
answer(context, set, set.tags.length === 0 ? `${ref} carries no tag now.` : `${ref}: #${set.tags.join(" #")}`);
|
|
2998
|
+
}
|
|
2999
|
+
};
|
|
2859
3000
|
const DOWNLOAD_URL = "https://github.com/Pixel-Vibing/pixel-vibing/releases/latest";
|
|
2860
3001
|
const EDITOR_VARIABLE = "PIXELVIBING_EDITOR";
|
|
2861
3002
|
async function exists$1(path) {
|
|
@@ -2920,9 +3061,6 @@ function drawLayerOf(band, foreground) {
|
|
|
2920
3061
|
if (foreground) return "foreground";
|
|
2921
3062
|
return band === "ground" ? "ground" : "object";
|
|
2922
3063
|
}
|
|
2923
|
-
function blank(width, height) {
|
|
2924
|
-
return { width, height, data: new Uint8ClampedArray(width * height * 4) };
|
|
2925
|
-
}
|
|
2926
3064
|
const SOLID = /^terrain\/ground\/([a-z0-9]+)$/;
|
|
2927
3065
|
const WANG = /^terrain\/([a-z0-9]+)-([a-z0-9]+)\/wang-(\d{2})$/;
|
|
2928
3066
|
function terrainTable(names) {
|
|
@@ -3900,60 +4038,14 @@ function drawCanvas(footprint) {
|
|
|
3900
4038
|
if (pxPerTile === void 0) return null;
|
|
3901
4039
|
return { width: footprint.cols * pxPerTile, height: footprint.rows * pxPerTile, pxPerTile };
|
|
3902
4040
|
}
|
|
3903
|
-
const CELLS_PER_TILE = 64;
|
|
3904
|
-
const MAX_CELL = 16;
|
|
3905
|
-
const MIN_CELL = 8;
|
|
3906
|
-
const ROUTE_MAX_EDGE = 3840;
|
|
3907
|
-
const ROUTE_MAX_PIXELS = 8294400;
|
|
3908
|
-
const ROUTE_MIN_PIXELS = 655360;
|
|
3909
|
-
const ROUTE_STEP = 16;
|
|
3910
|
-
function gridForCells(cols, rows, cellsPerTile = CELLS_PER_TILE) {
|
|
3911
|
-
if (!Number.isInteger(cols) || !Number.isInteger(rows) || cols <= 0 || rows <= 0) return null;
|
|
3912
|
-
const drawn = (cell) => cols * cell <= ROUTE_MAX_EDGE && rows * cell <= ROUTE_MAX_EDGE && cols * cell * rows * cell <= ROUTE_MAX_PIXELS && cols * cell % ROUTE_STEP === 0 && rows * cell % ROUTE_STEP === 0;
|
|
3913
|
-
const unscaled = (cell) => cols * cell * rows * cell >= ROUTE_MIN_PIXELS;
|
|
3914
|
-
const canvas = (cell) => ({ width: cols * cell, height: rows * cell, cell, pxPerTile: cellsPerTile * cell });
|
|
3915
|
-
if (drawn(MAX_CELL) && unscaled(MAX_CELL)) return canvas(MAX_CELL);
|
|
3916
|
-
if (drawn(MAX_CELL)) {
|
|
3917
|
-
for (let cell = MAX_CELL + 1; cell * Math.max(cols, rows) <= ROUTE_MAX_EDGE; cell += 1) if (drawn(cell) && unscaled(cell)) return canvas(cell);
|
|
3918
|
-
return null;
|
|
3919
|
-
}
|
|
3920
|
-
for (let cell = MAX_CELL - 1; cell >= MIN_CELL; cell -= 1) if (drawn(cell) && unscaled(cell)) return canvas(cell);
|
|
3921
|
-
return null;
|
|
3922
|
-
}
|
|
3923
|
-
const LEAST_CELLS_PER_TILE = 16;
|
|
3924
|
-
function cellCounts(cellsPerTile) {
|
|
3925
|
-
const counts = [cellsPerTile];
|
|
3926
|
-
for (let cells = cellsPerTile / 2; Number.isInteger(cells) && cells >= LEAST_CELLS_PER_TILE; cells /= 2) counts.push(cells);
|
|
3927
|
-
return counts;
|
|
3928
|
-
}
|
|
3929
|
-
function gridCanvas(footprint, cellsPerTile = CELLS_PER_TILE) {
|
|
3930
|
-
for (const cells of cellCounts(cellsPerTile)) {
|
|
3931
|
-
const canvas = gridForCells(footprint.cols * cells, footprint.rows * cells, cells);
|
|
3932
|
-
if (canvas !== null) return canvas;
|
|
3933
|
-
}
|
|
3934
|
-
return null;
|
|
3935
|
-
}
|
|
3936
|
-
function gridProblem(footprint, cellsPerTile = CELLS_PER_TILE) {
|
|
3937
|
-
if (gridCanvas(footprint, cellsPerTile) !== null) return null;
|
|
3938
|
-
const least = cellCounts(cellsPerTile).at(-1);
|
|
3939
|
-
const side = Math.floor(ROUTE_MAX_EDGE / (least * MIN_CELL));
|
|
3940
|
-
const area = Math.floor(ROUTE_MAX_PIXELS / (least * MIN_CELL) ** 2);
|
|
3941
|
-
return `a ${footprint.cols}×${footprint.rows} tile object is too large to draw on the grid: at most ${side} tile${side === 1 ? "" : "s"} on a side and ${area} tile${area === 1 ? "" : "s"} in all`;
|
|
3942
|
-
}
|
|
3943
|
-
function designCanvas(tiles) {
|
|
3944
|
-
if (!Number.isInteger(tiles.cols) || !Number.isInteger(tiles.rows) || tiles.cols < 1 || tiles.rows < 1) return null;
|
|
3945
|
-
const scale = scaleFor(tiles.cols, tiles.rows);
|
|
3946
|
-
if (scale === null) return null;
|
|
3947
|
-
return { cols: tiles.cols, rows: tiles.rows, scale, width: tiles.cols * scale, height: tiles.rows * scale };
|
|
3948
|
-
}
|
|
3949
4041
|
const GENERATED = {
|
|
3950
4042
|
object: { references: 8, words: "an object" },
|
|
3951
4043
|
creature: { references: 3, words: "a creature" },
|
|
3952
|
-
|
|
4044
|
+
[CONCEPT_ART]: { references: 8, words: "a concept art" },
|
|
3953
4045
|
icons: { references: 0, words: "an icon set" },
|
|
3954
4046
|
"ui-sketch": { references: 3, words: "a UI" }
|
|
3955
4047
|
};
|
|
3956
|
-
const KIND_NAMES = { ui: "ui-sketch" };
|
|
4048
|
+
const KIND_NAMES = { ui: "ui-sketch", [OLD_CONCEPT_ART]: CONCEPT_ART };
|
|
3957
4049
|
function listOf(given, what) {
|
|
3958
4050
|
const items = given.split(",").map((item) => item.trim());
|
|
3959
4051
|
if (items.some((item) => item === "")) throw new CliError(`${what} is a comma-separated list with no empty entry`);
|
|
@@ -4018,6 +4110,41 @@ async function sketchOf(context, path, instruction) {
|
|
|
4018
4110
|
};
|
|
4019
4111
|
}
|
|
4020
4112
|
const MAX_DESIGN_TILES = 64;
|
|
4113
|
+
function pixelsOf(given, what) {
|
|
4114
|
+
const match = /^(\d{1,4})[x×](\d{1,4})$/i.exec(given.trim());
|
|
4115
|
+
if (match === null) throw new CliError(`${what} is <width>x<height> in px, such as 1536x1024, not ${given}`);
|
|
4116
|
+
return { width: Number(match[1]), height: Number(match[2]) };
|
|
4117
|
+
}
|
|
4118
|
+
function conceptFields(args, asked, instruction) {
|
|
4119
|
+
const pixelArt = flagOf(args, "pixel-art") || asked === OLD_CONCEPT_ART;
|
|
4120
|
+
const transparent = flagOf(args, "transparent");
|
|
4121
|
+
const sizeGiven = stringOf(args, "size");
|
|
4122
|
+
const tilesGiven = stringOf(args, "tiles");
|
|
4123
|
+
const size = sizeGiven === void 0 ? void 0 : pixelsOf(sizeGiven, "--size");
|
|
4124
|
+
const tiles = tilesGiven === void 0 ? void 0 : sizeOf(tilesGiven, "--tiles");
|
|
4125
|
+
if (tiles !== void 0 && (tiles.cols > MAX_DESIGN_TILES || tiles.rows > MAX_DESIGN_TILES))
|
|
4126
|
+
throw new CliError(`a pixel-art concept art is 1 to ${MAX_DESIGN_TILES} tiles each way, not ${tiles.cols}×${tiles.rows}`);
|
|
4127
|
+
const problem = conceptShapeProblem({
|
|
4128
|
+
pixelArt,
|
|
4129
|
+
...size === void 0 ? {} : { size },
|
|
4130
|
+
...tiles === void 0 ? {} : { tiles },
|
|
4131
|
+
...transparent ? { background: "transparent" } : {}
|
|
4132
|
+
});
|
|
4133
|
+
if (problem !== null) throw new CliError(problem.replace("ask for pixel art too", "add --pixel-art"));
|
|
4134
|
+
const tags = stringsOf(args, "tag").map(tagOf);
|
|
4135
|
+
const tagged = tagsProblem(tags);
|
|
4136
|
+
if (tagged !== null) throw new CliError(tagged);
|
|
4137
|
+
const fields = {
|
|
4138
|
+
...pixelArt ? { pixelArt: true, tiles: tiles ?? DEFAULT_CONCEPT_TILES } : {},
|
|
4139
|
+
...size === void 0 ? {} : { size },
|
|
4140
|
+
...transparent ? { background: "transparent" } : {},
|
|
4141
|
+
...tags.length === 0 ? {} : { tags }
|
|
4142
|
+
};
|
|
4143
|
+
const drawn = tiles ?? DEFAULT_CONCEPT_TILES;
|
|
4144
|
+
const shown2 = size ?? DEFAULT_CONCEPT_SIZE;
|
|
4145
|
+
const what = pixelArt ? `a concept art ${drawn.cols}×${drawn.rows} tiles across, as pixel art` : `a concept art ${shown2.width}×${shown2.height} px${transparent ? ", its background cut away" : ""}`;
|
|
4146
|
+
return { fields, words: `${what}${tags.length === 0 ? "" : ` tagged ${tags.join(", ")}`}: ${instruction}` };
|
|
4147
|
+
}
|
|
4021
4148
|
const MAX_REFERENCE_BYTES = 16 * 1024 * 1024;
|
|
4022
4149
|
function sizeOf(given, what) {
|
|
4023
4150
|
const match = /^(\d{1,3})[x×](\d{1,3})$/i.exec(given.trim());
|
|
@@ -4049,6 +4176,10 @@ async function requestOf(context, asked, args) {
|
|
|
4049
4176
|
const paths = stringsOf(args, "reference");
|
|
4050
4177
|
if (paths.length > known.references)
|
|
4051
4178
|
throw new CliError(known.references === 0 ? `${known.words} takes no reference` : `${known.words} takes at most ${known.references} references`);
|
|
4179
|
+
if (kind !== CONCEPT_ART) {
|
|
4180
|
+
for (const flag of ["pixel-art", "transparent"]) if (flagOf(args, flag)) throw new CliError(`--${flag} is a concept art's`);
|
|
4181
|
+
if (stringsOf(args, "tag").length > 0) throw new CliError("--tag is a concept art's");
|
|
4182
|
+
}
|
|
4052
4183
|
if (kind === "ui-sketch") {
|
|
4053
4184
|
for (const flag of ["names", "purpose", "size", "tiles"])
|
|
4054
4185
|
if (stringOf(args, flag) !== void 0) throw new CliError(`--${flag} is not a UI's: its shapes are in the --sketch file`);
|
|
@@ -4065,13 +4196,17 @@ async function requestOf(context, asked, args) {
|
|
|
4065
4196
|
if (stringOf(args, "sketch") !== void 0) throw new CliError("--sketch is a UI's");
|
|
4066
4197
|
if (set) {
|
|
4067
4198
|
const { fields, words: words2 } = iconFields(args);
|
|
4068
|
-
if (stringOf(args, "tiles") !== void 0) throw new CliError("--tiles is a
|
|
4199
|
+
if (stringOf(args, "tiles") !== void 0) throw new CliError("--tiles is a pixel-art concept art's");
|
|
4069
4200
|
const body2 = { ...fields, ...instruction === "" ? {} : { instruction } };
|
|
4070
4201
|
return { kind, body: body2, words: instruction === "" ? words2 : `${words2}; ${instruction}` };
|
|
4071
4202
|
}
|
|
4072
4203
|
for (const flag of ["names", "purpose"]) if (stringOf(args, flag) !== void 0) throw new CliError(`--${flag} is an icon set's`);
|
|
4073
4204
|
const references = [];
|
|
4074
4205
|
for (const path of paths) references.push(await referenceOf(context, path));
|
|
4206
|
+
if (kind === CONCEPT_ART) {
|
|
4207
|
+
const { fields, words: words2 } = conceptFields(args, asked, instruction);
|
|
4208
|
+
return { kind, body: { instruction, ...fields }, ...references.length > 0 ? { images: { references } } : {}, words: words2 };
|
|
4209
|
+
}
|
|
4075
4210
|
const body = { instruction };
|
|
4076
4211
|
let words = `${known.words}: ${instruction}`;
|
|
4077
4212
|
if (kind === "object") {
|
|
@@ -4080,15 +4215,8 @@ async function requestOf(context, asked, args) {
|
|
|
4080
4215
|
if (problem !== null) throw new CliError(problem);
|
|
4081
4216
|
body.footprint = footprint;
|
|
4082
4217
|
words = `an object ${footprint.cols}×${footprint.rows} tiles: ${instruction}`;
|
|
4083
|
-
} else if (stringOf(args, "size") !== void 0) throw new CliError("--size is an object's");
|
|
4084
|
-
if (
|
|
4085
|
-
const tiles = sizeOf(stringOf(args, "tiles") ?? "6x4", "--tiles");
|
|
4086
|
-
if (tiles.cols > MAX_DESIGN_TILES || tiles.rows > MAX_DESIGN_TILES || designCanvas(tiles) === null) {
|
|
4087
|
-
throw new CliError(`a ${tiles.cols}×${tiles.rows} tile level design is not one that is drawn: 1 to ${MAX_DESIGN_TILES} tiles each way`);
|
|
4088
|
-
}
|
|
4089
|
-
body.tiles = tiles;
|
|
4090
|
-
words = `a level design ${tiles.cols}×${tiles.rows} tiles across: ${instruction}`;
|
|
4091
|
-
} else if (stringOf(args, "tiles") !== void 0) throw new CliError("--tiles is a level design's");
|
|
4218
|
+
} else if (stringOf(args, "size") !== void 0) throw new CliError("--size is an object's or a concept art's");
|
|
4219
|
+
if (stringOf(args, "tiles") !== void 0) throw new CliError("--tiles is a pixel-art concept art's");
|
|
4092
4220
|
return { kind, body, ...references.length > 0 ? { images: { references } } : {}, words };
|
|
4093
4221
|
}
|
|
4094
4222
|
async function confirmed(context, platform, what, prices, kind, yes) {
|
|
@@ -4118,8 +4246,8 @@ async function followed(context, platform, jobId) {
|
|
|
4118
4246
|
}
|
|
4119
4247
|
const generate = {
|
|
4120
4248
|
name: "generate",
|
|
4121
|
-
summary: "Generate an object, a creature,
|
|
4122
|
-
usage: "<object|creature|
|
|
4249
|
+
summary: "Generate an object, a creature, concept art, icons or a UI on the platform, then pull it",
|
|
4250
|
+
usage: "<object|creature|concept-art|icons|ui> [--instruction <text>] [--size <c>x<r>|<w>x<h>|<px>] [--transparent] [--pixel-art] [--tiles <c>x<r>] [--tag <tag>]… [--names <list>] [--purpose skill|item] [--sketch <json>] [--reference <png>]… [--yes]",
|
|
4123
4251
|
options: {
|
|
4124
4252
|
instruction: {
|
|
4125
4253
|
type: "string",
|
|
@@ -4129,11 +4257,19 @@ const generate = {
|
|
|
4129
4257
|
size: {
|
|
4130
4258
|
type: "string",
|
|
4131
4259
|
value: "<cols>x<rows>|<px>",
|
|
4132
|
-
description: "an object's footprint in tiles, 1x1 unless given; an icon's size, 16, 32 or 64 px, 32 unless given"
|
|
4260
|
+
description: "an object's footprint in tiles, 1x1 unless given; a concept art's size in px, multiples of 16 within 1536x1024 or 1024x1536, 1536x1024 unless given; an icon's size, 16, 32 or 64 px, 32 unless given"
|
|
4261
|
+
},
|
|
4262
|
+
transparent: { type: "boolean", description: "cut a concept art's background away, for one thing alone" },
|
|
4263
|
+
"pixel-art": { type: "boolean", description: "draw a concept art as pixel art some --tiles across, a level design to take apart" },
|
|
4264
|
+
tag: {
|
|
4265
|
+
type: "string",
|
|
4266
|
+
multiple: true,
|
|
4267
|
+
value: "<tag>",
|
|
4268
|
+
description: "what a concept art is for, such as anchor, ui, character, environment, prop or level-design; at most 8"
|
|
4133
4269
|
},
|
|
4134
4270
|
names: { type: "string", value: "<list>", description: `an icon set's icons, comma-separated, at most ${MAX_ICONS}` },
|
|
4135
4271
|
purpose: { type: "string", value: "skill|item", description: "whether an icon set is skills on badges or items alone; item unless given" },
|
|
4136
|
-
tiles: { type: "string", value: "<cols>x<rows>", description: "how many tiles a
|
|
4272
|
+
tiles: { type: "string", value: "<cols>x<rows>", description: "how many tiles a pixel-art concept art is across, 6x4 unless given" },
|
|
4137
4273
|
sketch: {
|
|
4138
4274
|
type: "string",
|
|
4139
4275
|
value: "<json>",
|
|
@@ -4616,7 +4752,7 @@ const move = {
|
|
|
4616
4752
|
);
|
|
4617
4753
|
}
|
|
4618
4754
|
};
|
|
4619
|
-
const version = "0.
|
|
4755
|
+
const version = "0.49.0";
|
|
4620
4756
|
const manifest = {
|
|
4621
4757
|
version
|
|
4622
4758
|
};
|
|
@@ -5098,6 +5234,7 @@ const COMMANDS = [
|
|
|
5098
5234
|
generate,
|
|
5099
5235
|
list,
|
|
5100
5236
|
pull,
|
|
5237
|
+
tag,
|
|
5101
5238
|
seal,
|
|
5102
5239
|
exportCommand,
|
|
5103
5240
|
editor,
|