@pietrovich/wot-utils 0.2.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/.env.example +3 -0
- package/LICENSE +13 -0
- package/README.md +109 -0
- package/dist/assets/fonts/pogs/3px.json5 +102 -0
- package/dist/assets/fonts/pogs/4px.json5 +436 -0
- package/dist/assets/fonts/pogs/numbers-bold.json5 +106 -0
- package/dist/assets/fonts/pogs/numbers.json5 +106 -0
- package/dist/assets/pogs/README.md +3 -0
- package/dist/assets/pogs/sheild.png +0 -0
- package/dist/assets/pogs/short-names.json5 +1133 -0
- package/dist/assets/pogs/star.png +0 -0
- package/dist/assets/pogs/stripe.png +0 -0
- package/dist/assets/pogs-fixed/pre-rendered/bg/at.bg.color.png +0 -0
- package/dist/assets/pogs-fixed/pre-rendered/bg/ht.bg.color.png +0 -0
- package/dist/assets/pogs-fixed/pre-rendered/bg/lt.bg.color.png +0 -0
- package/dist/assets/pogs-fixed/pre-rendered/bg/mt.bg.color.png +0 -0
- package/dist/assets/pogs-fixed/pre-rendered/bg/td.bg.color.png +0 -0
- package/dist/assets/pogs-fixed/pre-rendered/combined-v1/at.png +0 -0
- package/dist/assets/pogs-fixed/pre-rendered/combined-v1/at.prem.png +0 -0
- package/dist/assets/pogs-fixed/pre-rendered/combined-v1/generic.png +0 -0
- package/dist/assets/pogs-fixed/pre-rendered/combined-v1/generic.prem.png +0 -0
- package/dist/assets/pogs-fixed/pre-rendered/combined-v1/ht.png +0 -0
- package/dist/assets/pogs-fixed/pre-rendered/combined-v1/ht.prem.png +0 -0
- package/dist/assets/pogs-fixed/pre-rendered/combined-v1/lt.png +0 -0
- package/dist/assets/pogs-fixed/pre-rendered/combined-v1/lt.prem.png +0 -0
- package/dist/assets/pogs-fixed/pre-rendered/combined-v1/mt.png +0 -0
- package/dist/assets/pogs-fixed/pre-rendered/combined-v1/mt.prem.png +0 -0
- package/dist/assets/pogs-fixed/pre-rendered/combined-v1/td.png +0 -0
- package/dist/assets/pogs-fixed/pre-rendered/combined-v1/td.prem.png +0 -0
- package/dist/assets/pogs-fixed/pre-rendered/combined-v2/at.clear.png +0 -0
- package/dist/assets/pogs-fixed/pre-rendered/combined-v2/at.clear.prem.png +0 -0
- package/dist/assets/pogs-fixed/pre-rendered/combined-v2/at.png +0 -0
- package/dist/assets/pogs-fixed/pre-rendered/combined-v2/at.prem.png +0 -0
- package/dist/assets/pogs-fixed/pre-rendered/combined-v2/ht.clear-prem.png +0 -0
- package/dist/assets/pogs-fixed/pre-rendered/combined-v2/ht.clear.png +0 -0
- package/dist/assets/pogs-fixed/pre-rendered/combined-v2/ht.clear.prem.png +0 -0
- package/dist/assets/pogs-fixed/pre-rendered/combined-v2/ht.png +0 -0
- package/dist/assets/pogs-fixed/pre-rendered/combined-v2/ht.prem.png +0 -0
- package/dist/assets/pogs-fixed/pre-rendered/combined-v2/lt.clear.png +0 -0
- package/dist/assets/pogs-fixed/pre-rendered/combined-v2/lt.clear.prem.png +0 -0
- package/dist/assets/pogs-fixed/pre-rendered/combined-v2/lt.png +0 -0
- package/dist/assets/pogs-fixed/pre-rendered/combined-v2/lt.prem.png +0 -0
- package/dist/assets/pogs-fixed/pre-rendered/combined-v2/mt.clear.png +0 -0
- package/dist/assets/pogs-fixed/pre-rendered/combined-v2/mt.clear.prem.png +0 -0
- package/dist/assets/pogs-fixed/pre-rendered/combined-v2/mt.png +0 -0
- package/dist/assets/pogs-fixed/pre-rendered/combined-v2/mt.prem.png +0 -0
- package/dist/assets/pogs-fixed/pre-rendered/combined-v2/td.clear.png +0 -0
- package/dist/assets/pogs-fixed/pre-rendered/combined-v2/td.clear.prem.png +0 -0
- package/dist/assets/pogs-fixed/pre-rendered/combined-v2/td.png +0 -0
- package/dist/assets/pogs-fixed/pre-rendered/combined-v2/td.prem.png +0 -0
- package/dist/assets/pogs-fixed/pre-rendered/stripes/generic.png +0 -0
- package/dist/assets/pogs-fixed/pre-rendered/stripes/ht.png +0 -0
- package/dist/assets/pogs-fixed/pre-rendered/stripes/lt.png +0 -0
- package/dist/assets/pogs-fixed/pre-rendered/stripes/mt.png +0 -0
- package/dist/assets/pogs-fixed/pre-rendered/stripes/spg.png +0 -0
- package/dist/assets/pogs-fixed/pre-rendered/stripes/td.png +0 -0
- package/dist/index.js +2958 -0
- package/package.json +62 -0
- package/scripts/bake-color-dmg-fsr-vr-rld.sh +81 -0
- package/scripts/bake-pogs-clear.sh +7 -0
- package/scripts/bake-pogs-color-dmg-fsr-vr-rld.sh +7 -0
- package/scripts/baker-lib.sh +165 -0
- package/scripts/extract-atlas-assets.ps1 +120 -0
- package/scripts/extract-atlas-assets.sh +116 -0
- package/scripts/replace-suffixed-with-base.sh +44 -0
package/dist/index.js
ADDED
|
@@ -0,0 +1,2958 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
// src/index.ts
|
|
4
|
+
import "module";
|
|
5
|
+
import { config } from "dotenv";
|
|
6
|
+
import { resolve as resolve7 } from "path";
|
|
7
|
+
import { Command as Command21 } from "commander";
|
|
8
|
+
|
|
9
|
+
// src/lib/WGData.ts
|
|
10
|
+
import { mkdirSync } from "fs";
|
|
11
|
+
import { writeFile as writeFile2, access } from "fs/promises";
|
|
12
|
+
import { isAbsolute as isAbsolute2, join as join2, basename, resolve as resolve2 } from "path";
|
|
13
|
+
import sharp from "sharp";
|
|
14
|
+
|
|
15
|
+
// src/lib/config.ts
|
|
16
|
+
function getAppId() {
|
|
17
|
+
const appId = process.env.WG_APP_ID;
|
|
18
|
+
if (!appId) {
|
|
19
|
+
console.error("Error: No application ID. Set WG_APP_ID in .env.");
|
|
20
|
+
process.exit(1);
|
|
21
|
+
}
|
|
22
|
+
return appId;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
// src/lib/api.ts
|
|
26
|
+
var WGApiError = class extends Error {
|
|
27
|
+
constructor(field, code, message) {
|
|
28
|
+
super(message);
|
|
29
|
+
this.field = field;
|
|
30
|
+
this.code = code;
|
|
31
|
+
this.name = "WGApiError";
|
|
32
|
+
}
|
|
33
|
+
field;
|
|
34
|
+
code;
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
// src/lib/logger.ts
|
|
38
|
+
import pino from "pino";
|
|
39
|
+
var level = process.env.DEBUG ? "debug" : "info";
|
|
40
|
+
var logger = process.env.LOG_FILE ? pino({ level }, pino.destination(process.env.LOG_FILE)) : pino({
|
|
41
|
+
level,
|
|
42
|
+
transport: {
|
|
43
|
+
target: "pino-pretty",
|
|
44
|
+
options: { destination: 2, colorize: true }
|
|
45
|
+
}
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
// src/lib/cache.ts
|
|
49
|
+
import { createHash } from "crypto";
|
|
50
|
+
import { mkdir, readFile, writeFile, rm } from "fs/promises";
|
|
51
|
+
import { isAbsolute, join, resolve } from "path";
|
|
52
|
+
function getCacheDir() {
|
|
53
|
+
const dir = process.env.WG_CACHE_DIR ?? ".data/cache";
|
|
54
|
+
if (isAbsolute(dir)) {
|
|
55
|
+
return dir;
|
|
56
|
+
}
|
|
57
|
+
return resolve(process.env.PIE_WOT_CWD ?? process.cwd(), dir);
|
|
58
|
+
}
|
|
59
|
+
function cacheKey(prefix, endpoint, params) {
|
|
60
|
+
const sorted = Object.entries(params).sort(([a], [b]) => a.localeCompare(b));
|
|
61
|
+
const input = `${endpoint}:${JSON.stringify(sorted)}`;
|
|
62
|
+
const hash = createHash("sha256").update(input).digest("hex").slice(0, 16);
|
|
63
|
+
return `${prefix}-${hash}`;
|
|
64
|
+
}
|
|
65
|
+
async function getCached(prefix, endpoint, params = {}) {
|
|
66
|
+
const file = join(getCacheDir(), `${cacheKey(prefix, endpoint, params)}.json`);
|
|
67
|
+
try {
|
|
68
|
+
const raw = await readFile(file, "utf-8");
|
|
69
|
+
return JSON.parse(raw).data;
|
|
70
|
+
} catch {
|
|
71
|
+
return null;
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
async function setCached(prefix, endpoint, params, data) {
|
|
75
|
+
const dir = getCacheDir();
|
|
76
|
+
await mkdir(dir, { recursive: true });
|
|
77
|
+
const file = join(dir, `${cacheKey(prefix, endpoint, params)}.json`);
|
|
78
|
+
const entry = { fetchedAt: (/* @__PURE__ */ new Date()).toISOString(), data };
|
|
79
|
+
await writeFile(file, JSON.stringify(entry, null, 2));
|
|
80
|
+
}
|
|
81
|
+
async function purgeCache() {
|
|
82
|
+
await rm(getCacheDir(), { recursive: true, force: true });
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
// src/lib/WGData.ts
|
|
86
|
+
var IMAGE_KEY = {
|
|
87
|
+
small: "small_icon",
|
|
88
|
+
medium: "contour_icon",
|
|
89
|
+
large: "big_icon",
|
|
90
|
+
xs: "contour_icon"
|
|
91
|
+
// derived from medium — cropped locally, not fetched from WG
|
|
92
|
+
};
|
|
93
|
+
async function fileExists(path) {
|
|
94
|
+
try {
|
|
95
|
+
await access(path);
|
|
96
|
+
return true;
|
|
97
|
+
} catch {
|
|
98
|
+
return false;
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
async function downloadIcon(url, dest) {
|
|
102
|
+
const response = await fetch(url);
|
|
103
|
+
if (!response.ok) {
|
|
104
|
+
throw new Error(`HTTP ${response.status}`);
|
|
105
|
+
}
|
|
106
|
+
const buffer = Buffer.from(await response.arrayBuffer());
|
|
107
|
+
await writeFile2(dest, buffer);
|
|
108
|
+
}
|
|
109
|
+
var WGData = class {
|
|
110
|
+
appId;
|
|
111
|
+
limitOutput = 1e4;
|
|
112
|
+
iconsBaseDir;
|
|
113
|
+
vehicles = null;
|
|
114
|
+
constructor() {
|
|
115
|
+
this.appId = getAppId();
|
|
116
|
+
this.limitOutput = process.env.LIMIT_OUTPUT ? Number(process.env.LIMIT_OUTPUT) : 3;
|
|
117
|
+
const rawCacheDir = process.env.WG_CACHE_DIR ?? ".data/wg/cache";
|
|
118
|
+
const cacheDir = isAbsolute2(rawCacheDir) ? rawCacheDir : resolve2(process.env.PIE_WOT_CWD ?? process.cwd(), rawCacheDir);
|
|
119
|
+
this.iconsBaseDir = join2(cacheDir, "..", "icons");
|
|
120
|
+
for (const size of Object.keys(IMAGE_KEY)) {
|
|
121
|
+
mkdirSync(join2(this.iconsBaseDir, size), { recursive: true });
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
async getVehicles() {
|
|
125
|
+
if (!this.vehicles) {
|
|
126
|
+
const endpoint = "encyclopedia/vehicles";
|
|
127
|
+
const cached = await getCached("list-vehicles", endpoint);
|
|
128
|
+
if (cached) {
|
|
129
|
+
this.vehicles = cached;
|
|
130
|
+
logger.debug("vehicles: cache hit");
|
|
131
|
+
return Object.values(cached);
|
|
132
|
+
}
|
|
133
|
+
logger.debug("vehicles: fetching from API");
|
|
134
|
+
this.vehicles = await this.fetchVehicles();
|
|
135
|
+
await setCached("list-vehicles", endpoint, {}, this.vehicles);
|
|
136
|
+
}
|
|
137
|
+
return Object.values(this.vehicles);
|
|
138
|
+
}
|
|
139
|
+
async exportVehicles({ output, useCache = true } = {}) {
|
|
140
|
+
const endpoint = "encyclopedia/vehicles";
|
|
141
|
+
const params = { application_id: this.appId };
|
|
142
|
+
const outputPath = output ?? `wg-export-${(/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-")}.json`;
|
|
143
|
+
let data = null;
|
|
144
|
+
if (useCache) {
|
|
145
|
+
data = await getCached("export", endpoint, params);
|
|
146
|
+
}
|
|
147
|
+
if (!data) {
|
|
148
|
+
data = await this.fetchVehicles();
|
|
149
|
+
if (useCache) {
|
|
150
|
+
await setCached("export", endpoint, params, data);
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
await writeFile2(outputPath, JSON.stringify(data, null, 2));
|
|
154
|
+
console.error(`Exported ${Object.keys(data).length} vehicles to ${outputPath}`);
|
|
155
|
+
}
|
|
156
|
+
async fetchVehicleIcon(vehicle2, size, force = false) {
|
|
157
|
+
const url = vehicle2.images?.[IMAGE_KEY[size]];
|
|
158
|
+
if (!url) {
|
|
159
|
+
return { skipped: true, downloaded: false, failed: false, path: null, error: null };
|
|
160
|
+
}
|
|
161
|
+
const dest = join2(this.iconsBaseDir, size, basename(url));
|
|
162
|
+
if (!force && await fileExists(dest)) {
|
|
163
|
+
logger.debug({ tag: vehicle2.tag, size, dest }, "icon: cached");
|
|
164
|
+
return { skipped: true, downloaded: false, failed: false, path: dest, error: null };
|
|
165
|
+
}
|
|
166
|
+
logger.debug({ tag: vehicle2.tag, size, url }, "icon: downloading");
|
|
167
|
+
try {
|
|
168
|
+
await downloadIcon(url, dest);
|
|
169
|
+
if (size === "xs") {
|
|
170
|
+
const { data: trimmed, info } = await sharp(dest).trim().toBuffer({ resolveWithObject: true });
|
|
171
|
+
const SCALE_FACTOR = 0.8;
|
|
172
|
+
const scaled = await sharp(trimmed).resize(Math.round(info.width * SCALE_FACTOR), Math.round(info.height * SCALE_FACTOR)).toBuffer();
|
|
173
|
+
await writeFile2(dest, scaled);
|
|
174
|
+
}
|
|
175
|
+
return { skipped: false, downloaded: true, failed: false, path: dest, error: null };
|
|
176
|
+
} catch (err) {
|
|
177
|
+
return { skipped: false, downloaded: false, failed: true, path: null, error: err instanceof Error ? err.message : String(err) };
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
async getDefaultVehicleIcon(vehicle2, size) {
|
|
181
|
+
const result = await this.fetchVehicleIcon(vehicle2, size, false);
|
|
182
|
+
if (!result.path) {
|
|
183
|
+
return null;
|
|
184
|
+
}
|
|
185
|
+
return sharp(result.path).ensureAlpha().raw().toBuffer({ resolveWithObject: true });
|
|
186
|
+
}
|
|
187
|
+
async fetchIcons({ force = false, concurrency = 10, size, query }) {
|
|
188
|
+
const vehicles = query ? [await this.findVehicle(query)] : await this.getVehicles();
|
|
189
|
+
let downloaded = 0;
|
|
190
|
+
let skipped = 0;
|
|
191
|
+
let failed = 0;
|
|
192
|
+
for (let i = 0; i < vehicles.length; i += concurrency) {
|
|
193
|
+
const batch = vehicles.slice(i, i + concurrency);
|
|
194
|
+
await Promise.all(
|
|
195
|
+
batch.map(async (vehicle2) => {
|
|
196
|
+
const result = await this.fetchVehicleIcon(vehicle2, size, force);
|
|
197
|
+
const name = vehicle2.images?.[IMAGE_KEY[size]] ? basename(vehicle2.images[IMAGE_KEY[size]]) : vehicle2.tag;
|
|
198
|
+
if (result.downloaded) {
|
|
199
|
+
downloaded++;
|
|
200
|
+
console.error(`\u2193 ${name}`);
|
|
201
|
+
} else if (result.failed) {
|
|
202
|
+
failed++;
|
|
203
|
+
console.error(`\u2717 ${name}: ${result.error}`);
|
|
204
|
+
} else {
|
|
205
|
+
skipped++;
|
|
206
|
+
}
|
|
207
|
+
})
|
|
208
|
+
);
|
|
209
|
+
}
|
|
210
|
+
console.error(`
|
|
211
|
+
Done: ${downloaded} downloaded, ${skipped} skipped, ${failed} failed`);
|
|
212
|
+
}
|
|
213
|
+
getBestConfig(vehicle2) {
|
|
214
|
+
const tree = vehicle2.modules_tree;
|
|
215
|
+
const parent = /* @__PURE__ */ new Map();
|
|
216
|
+
for (const node of Object.values(tree)) {
|
|
217
|
+
if (node.next_modules) {
|
|
218
|
+
for (const childId of node.next_modules) {
|
|
219
|
+
parent.set(childId, node.module_id);
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
const depths = /* @__PURE__ */ new Map();
|
|
224
|
+
const depth = (id) => {
|
|
225
|
+
let d = depths.get(id);
|
|
226
|
+
if (d === void 0) {
|
|
227
|
+
const p = parent.get(id);
|
|
228
|
+
d = p === void 0 ? 0 : 1 + depth(p);
|
|
229
|
+
depths.set(id, d);
|
|
230
|
+
}
|
|
231
|
+
return d;
|
|
232
|
+
};
|
|
233
|
+
const byType = /* @__PURE__ */ new Map();
|
|
234
|
+
for (const node of Object.values(tree)) {
|
|
235
|
+
const current = byType.get(node.type);
|
|
236
|
+
if (current === void 0) {
|
|
237
|
+
byType.set(node.type, node);
|
|
238
|
+
} else {
|
|
239
|
+
const nd = depth(node.module_id);
|
|
240
|
+
const cd = depth(current.module_id);
|
|
241
|
+
if (nd > cd || nd === cd && node.module_id > current.module_id) {
|
|
242
|
+
byType.set(node.type, node);
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
const result = {};
|
|
247
|
+
for (const [type, node] of byType) {
|
|
248
|
+
result[type] = node.module_id;
|
|
249
|
+
}
|
|
250
|
+
logger.debug({ tank_id: vehicle2.tank_id, config: result }, "best config");
|
|
251
|
+
return result;
|
|
252
|
+
}
|
|
253
|
+
async inferBestConfig(query) {
|
|
254
|
+
return this.getBestConfig(await this.findVehicle(query));
|
|
255
|
+
}
|
|
256
|
+
configToProfileId(config2) {
|
|
257
|
+
return Object.values(config2).filter((id) => id != null).sort((a, b) => a - b).join("-");
|
|
258
|
+
}
|
|
259
|
+
async getStatsForBestConfig(query) {
|
|
260
|
+
const vehicle2 = typeof query === "object" ? query : await this.findVehicle(query);
|
|
261
|
+
const config2 = this.getBestConfig(vehicle2);
|
|
262
|
+
const profileId = this.configToProfileId(config2);
|
|
263
|
+
const endpoint = "encyclopedia/vehicleprofile";
|
|
264
|
+
const params = {
|
|
265
|
+
application_id: this.appId,
|
|
266
|
+
tank_id: String(vehicle2.tank_id),
|
|
267
|
+
profile_id: profileId
|
|
268
|
+
};
|
|
269
|
+
const cacheFilePrefix = `vehicle-${String(vehicle2.tank_id).padStart(5, "0")}-profile`;
|
|
270
|
+
const cached = await getCached(cacheFilePrefix, endpoint, params);
|
|
271
|
+
if (cached) {
|
|
272
|
+
logger.debug({ tank_id: vehicle2.tank_id, profileId }, "profile: cache hit");
|
|
273
|
+
return cached;
|
|
274
|
+
}
|
|
275
|
+
logger.debug({ tank_id: vehicle2.tank_id, profileId }, "profile: fetching from API");
|
|
276
|
+
const url = new URL(`https://api.worldoftanks.eu/wot/${endpoint}/`);
|
|
277
|
+
for (const [k, v] of Object.entries(params)) {
|
|
278
|
+
url.searchParams.set(k, v);
|
|
279
|
+
}
|
|
280
|
+
const response = await fetch(url.toString());
|
|
281
|
+
const json = await response.json();
|
|
282
|
+
if (json.status === "error") {
|
|
283
|
+
const err = json.error;
|
|
284
|
+
logger.warn({ params }, "failed to find profile");
|
|
285
|
+
throw new WGApiError(err.field, err.code, err.message);
|
|
286
|
+
}
|
|
287
|
+
const data = json.data;
|
|
288
|
+
await setCached(cacheFilePrefix, endpoint, params, data);
|
|
289
|
+
return data;
|
|
290
|
+
}
|
|
291
|
+
async getShortNameStats() {
|
|
292
|
+
const vehicles = await this.getVehicles();
|
|
293
|
+
const chars = /* @__PURE__ */ new Set();
|
|
294
|
+
let longestShortName = "";
|
|
295
|
+
const lengths = [];
|
|
296
|
+
for (const v of vehicles) {
|
|
297
|
+
for (const c of v.short_name) {
|
|
298
|
+
chars.add(c);
|
|
299
|
+
}
|
|
300
|
+
lengths.push(v.short_name.length);
|
|
301
|
+
if (v.short_name.length > longestShortName.length) {
|
|
302
|
+
longestShortName = v.short_name;
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
const sorted = [...lengths].sort((a, b) => a - b);
|
|
306
|
+
const n = sorted.length;
|
|
307
|
+
const avgLength = Math.round(lengths.reduce((s, l) => s + l, 0) / n);
|
|
308
|
+
const medianLength = n % 2 === 0 ? (sorted[n / 2 - 1] + sorted[n / 2]) / 2 : sorted[Math.floor(n / 2)];
|
|
309
|
+
const p80Length = sorted[Math.ceil(0.8 * n) - 1];
|
|
310
|
+
const p90Length = sorted[Math.ceil(0.9 * n) - 1];
|
|
311
|
+
return {
|
|
312
|
+
uniqueCharacters: [...chars].sort().join(""),
|
|
313
|
+
maxLength: sorted[n - 1],
|
|
314
|
+
longestShortName,
|
|
315
|
+
avgLength,
|
|
316
|
+
medianLength,
|
|
317
|
+
p80Length,
|
|
318
|
+
p90Length
|
|
319
|
+
};
|
|
320
|
+
}
|
|
321
|
+
async purgeCache() {
|
|
322
|
+
await purgeCache();
|
|
323
|
+
}
|
|
324
|
+
async findVehicle(query) {
|
|
325
|
+
const vehicles = await this.getVehicles();
|
|
326
|
+
const q = String(query);
|
|
327
|
+
const isId = /^\d+$/.test(q);
|
|
328
|
+
const vehicle2 = vehicles.find((v) => {
|
|
329
|
+
if (isId) {
|
|
330
|
+
return v.tank_id === Number(q);
|
|
331
|
+
}
|
|
332
|
+
return v.tag === q || v.short_name === q;
|
|
333
|
+
});
|
|
334
|
+
if (!vehicle2) {
|
|
335
|
+
throw new Error(`Vehicle not found: ${query}`);
|
|
336
|
+
}
|
|
337
|
+
logger.debug({ tank_id: vehicle2.tank_id, tag: vehicle2.tag }, "vehicle found");
|
|
338
|
+
return vehicle2;
|
|
339
|
+
}
|
|
340
|
+
async fetchVehicles() {
|
|
341
|
+
const fields = [
|
|
342
|
+
"-crew",
|
|
343
|
+
"-default_profile",
|
|
344
|
+
"-description",
|
|
345
|
+
"-prices_xp",
|
|
346
|
+
"-price_gold",
|
|
347
|
+
"-price_credit",
|
|
348
|
+
"-next_tanks"
|
|
349
|
+
].join(",");
|
|
350
|
+
const url = new URL("https://api.worldoftanks.eu/wot/encyclopedia/vehicles/");
|
|
351
|
+
url.searchParams.set("application_id", this.appId);
|
|
352
|
+
const tmp = `${url.toString()}&fields=${fields}`;
|
|
353
|
+
const response = await fetch(tmp);
|
|
354
|
+
const json = await response.json();
|
|
355
|
+
if (json.status === "error") {
|
|
356
|
+
const err = json.error;
|
|
357
|
+
throw new WGApiError(err.field, err.code, err.message);
|
|
358
|
+
}
|
|
359
|
+
return json.data;
|
|
360
|
+
}
|
|
361
|
+
};
|
|
362
|
+
|
|
363
|
+
// src/commands/vehicle/list.ts
|
|
364
|
+
import { Command } from "commander";
|
|
365
|
+
|
|
366
|
+
// src/lib/format.ts
|
|
367
|
+
import Table from "cli-table3";
|
|
368
|
+
function printJson(data) {
|
|
369
|
+
console.log(JSON.stringify(data, null, 2));
|
|
370
|
+
}
|
|
371
|
+
var GREEN = "\x1B[32m";
|
|
372
|
+
var RESET = "\x1B[0m";
|
|
373
|
+
function printVehiclesTable(vehicles) {
|
|
374
|
+
const table = new Table({
|
|
375
|
+
head: ["Short", "Tag", "Name", "ID", ""],
|
|
376
|
+
colAligns: ["left", "left", "left", "right", "left"]
|
|
377
|
+
});
|
|
378
|
+
for (const v of vehicles) {
|
|
379
|
+
table.push([v.short_name, v.tag, v.name, v.tank_id, `${GREEN}\u2713${RESET}`]);
|
|
380
|
+
}
|
|
381
|
+
console.log(table.toString());
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
// src/commands/vehicle/list.ts
|
|
385
|
+
function listVehiclesCommand(app2) {
|
|
386
|
+
return new Command("list").description("List vehicles from the WoT encyclopedia").option("--all", "show all vehicles (default: first 3)").option("--json", "output as JSON").action(async (options) => {
|
|
387
|
+
try {
|
|
388
|
+
const vehicles = await app2.getVehicles();
|
|
389
|
+
const items = options.all ? vehicles : vehicles.slice(0, 3);
|
|
390
|
+
if (options.json) {
|
|
391
|
+
printJson(items);
|
|
392
|
+
} else {
|
|
393
|
+
printVehiclesTable(items);
|
|
394
|
+
}
|
|
395
|
+
} catch (error) {
|
|
396
|
+
if (error instanceof WGApiError) {
|
|
397
|
+
console.error(`API error [${error.code}] ${error.field}: ${error.message}`);
|
|
398
|
+
} else {
|
|
399
|
+
console.error("Error:", error instanceof Error ? error.message : error);
|
|
400
|
+
}
|
|
401
|
+
process.exit(1);
|
|
402
|
+
}
|
|
403
|
+
});
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
// src/commands/vehicle/export.ts
|
|
407
|
+
import { Command as Command2 } from "commander";
|
|
408
|
+
function exportCommand(app2) {
|
|
409
|
+
return new Command2("export").description("Export all vehicles to a JSON file").option("--output <path>", "output file path (default: wg-export-<timestamp>.json)").option("--no-cache", "bypass cache and fetch fresh data").action(async (options) => {
|
|
410
|
+
try {
|
|
411
|
+
await app2.exportVehicles({ output: options.output, useCache: options.cache });
|
|
412
|
+
} catch (error) {
|
|
413
|
+
if (error instanceof WGApiError) {
|
|
414
|
+
console.error(`API error [${error.code}] ${error.field}: ${error.message}`);
|
|
415
|
+
} else {
|
|
416
|
+
console.error("Error:", error instanceof Error ? error.message : error);
|
|
417
|
+
}
|
|
418
|
+
process.exit(1);
|
|
419
|
+
}
|
|
420
|
+
});
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
// src/commands/cache/purge.ts
|
|
424
|
+
import { Command as Command3 } from "commander";
|
|
425
|
+
function cachePurgeCommand(app2) {
|
|
426
|
+
return new Command3("purge").description("Delete all cached API responses").action(async () => {
|
|
427
|
+
await app2.purgeCache();
|
|
428
|
+
console.error("Cache purged.");
|
|
429
|
+
});
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
// src/commands/vehicle/best-config.ts
|
|
433
|
+
import { Command as Command4 } from "commander";
|
|
434
|
+
function bestConfigCommand(app2) {
|
|
435
|
+
return new Command4("best-config").description("Infer best module configuration for a vehicle").argument("<query>", "tank_id (number), tag, or short_name").action(async (query) => {
|
|
436
|
+
try {
|
|
437
|
+
const result = await app2.inferBestConfig(query);
|
|
438
|
+
console.log(app2.configToProfileId(result));
|
|
439
|
+
} catch (error) {
|
|
440
|
+
if (error instanceof WGApiError) {
|
|
441
|
+
console.error(`API error [${error.code}] ${error.field}: ${error.message}`);
|
|
442
|
+
} else {
|
|
443
|
+
console.error("Error:", error instanceof Error ? error.message : error);
|
|
444
|
+
}
|
|
445
|
+
process.exit(1);
|
|
446
|
+
}
|
|
447
|
+
});
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
// src/commands/vehicle/stats.ts
|
|
451
|
+
import { Command as Command5 } from "commander";
|
|
452
|
+
function formatArmor(a) {
|
|
453
|
+
try {
|
|
454
|
+
return `${a.front} / ${a.sides} / ${a.rear}`;
|
|
455
|
+
} catch {
|
|
456
|
+
return "";
|
|
457
|
+
}
|
|
458
|
+
}
|
|
459
|
+
function printProfile(profile) {
|
|
460
|
+
console.log(`profile: ${profile.profile_id}`);
|
|
461
|
+
console.log(`view range: ${profile.turret.view_range} m`);
|
|
462
|
+
console.log(`hull armor: ${formatArmor(profile.armor.hull)} (front / sides / rear)`);
|
|
463
|
+
console.log(`turret armor: ${formatArmor(profile.armor.turret)} (front / sides / rear)`);
|
|
464
|
+
console.log(`reload time: ${profile.gun.reload_time.toFixed(2)} s`);
|
|
465
|
+
}
|
|
466
|
+
function printTable(rows) {
|
|
467
|
+
const headers = ["Name", "View Range", "Reload (s)", "Hull (f/s/r)", "Turret (f/s/r)"];
|
|
468
|
+
const cells = rows.map((r) => [
|
|
469
|
+
r.name,
|
|
470
|
+
String(r.viewRange),
|
|
471
|
+
r.reloadTime.toFixed(2),
|
|
472
|
+
formatArmor(r.hullArmor),
|
|
473
|
+
formatArmor(r.turretArmor)
|
|
474
|
+
]);
|
|
475
|
+
const widths = headers.map((h, i) => Math.max(h.length, ...cells.map((row) => row[i].length)));
|
|
476
|
+
const pad = (s, w) => s.padEnd(w);
|
|
477
|
+
const formatRow = (row) => row.map((cell, i) => pad(cell, widths[i])).join(" ");
|
|
478
|
+
console.log(formatRow(headers));
|
|
479
|
+
console.log(widths.map((w) => "-".repeat(w)).join(" "));
|
|
480
|
+
for (const row of cells) {
|
|
481
|
+
try {
|
|
482
|
+
console.log(formatRow(row));
|
|
483
|
+
} catch (e) {
|
|
484
|
+
console.log(`!!!! ${row[0]} ${e instanceof Error ? e.message : e}`);
|
|
485
|
+
}
|
|
486
|
+
}
|
|
487
|
+
}
|
|
488
|
+
function extractProfile(data) {
|
|
489
|
+
return Object.values(data)[0];
|
|
490
|
+
}
|
|
491
|
+
function vehicleStatsCommand(app2) {
|
|
492
|
+
return new Command5("stats").description("Fetch stats for the best module configuration of a vehicle").argument("[query]", "tank_id (number), tag, or short_name").option("--all", "fetch stats for all vehicles and print as a table").option("-q, --quiet", "suppress progress output").option("--json", "output results as JSON array instead of a table (use with --all)").option("--raw", "print full JSON response (single vehicle only)").action(async (query, options) => {
|
|
493
|
+
try {
|
|
494
|
+
if (!query || options.all) {
|
|
495
|
+
const vehicles = await app2.getVehicles();
|
|
496
|
+
const sorted = [...vehicles].sort((a, b) => a.short_name.localeCompare(b.short_name));
|
|
497
|
+
const targets = options.all ? sorted : sorted.slice(0, 10);
|
|
498
|
+
const rows = [];
|
|
499
|
+
const batchSize = 5;
|
|
500
|
+
for (let i = 0; i < targets.length; i += batchSize) {
|
|
501
|
+
const batch = targets.slice(i, i + batchSize);
|
|
502
|
+
if (!options.raw && !options.quiet) {
|
|
503
|
+
console.error(`${i + 1}\u2013${Math.min(i + batchSize, targets.length)} / ${targets.length}`);
|
|
504
|
+
}
|
|
505
|
+
const results = await Promise.all(
|
|
506
|
+
batch.map(async (v) => {
|
|
507
|
+
try {
|
|
508
|
+
const data = await app2.getStatsForBestConfig(v.tank_id);
|
|
509
|
+
const profile = extractProfile(data);
|
|
510
|
+
return {
|
|
511
|
+
name: v.short_name,
|
|
512
|
+
viewRange: profile.turret.view_range,
|
|
513
|
+
reloadTime: profile.gun.reload_time,
|
|
514
|
+
hullArmor: profile.armor.hull,
|
|
515
|
+
turretArmor: profile.armor.turret
|
|
516
|
+
};
|
|
517
|
+
} catch {
|
|
518
|
+
return null;
|
|
519
|
+
}
|
|
520
|
+
})
|
|
521
|
+
);
|
|
522
|
+
for (const row of results) {
|
|
523
|
+
if (row) {
|
|
524
|
+
rows.push(row);
|
|
525
|
+
}
|
|
526
|
+
}
|
|
527
|
+
}
|
|
528
|
+
if (options.json) {
|
|
529
|
+
console.log(JSON.stringify(rows, null, 2));
|
|
530
|
+
} else {
|
|
531
|
+
printTable(rows);
|
|
532
|
+
}
|
|
533
|
+
return;
|
|
534
|
+
}
|
|
535
|
+
const result = await app2.getStatsForBestConfig(query);
|
|
536
|
+
if (options.raw) {
|
|
537
|
+
console.log(JSON.stringify(result, null, 2));
|
|
538
|
+
return;
|
|
539
|
+
}
|
|
540
|
+
printProfile(extractProfile(result));
|
|
541
|
+
} catch (error) {
|
|
542
|
+
if (error instanceof WGApiError) {
|
|
543
|
+
console.error(`API error [${error.code}] ${error.field}: ${error.message}`);
|
|
544
|
+
} else {
|
|
545
|
+
console.error("Error:", error instanceof Error ? error.message : error);
|
|
546
|
+
}
|
|
547
|
+
process.exit(1);
|
|
548
|
+
}
|
|
549
|
+
});
|
|
550
|
+
}
|
|
551
|
+
|
|
552
|
+
// src/commands/vehicle/chars.ts
|
|
553
|
+
import { Command as Command6 } from "commander";
|
|
554
|
+
function charsCommand(app2) {
|
|
555
|
+
return new Command6("chars").description("List unique characters found in vehicle short names").action(async () => {
|
|
556
|
+
try {
|
|
557
|
+
const { uniqueCharacters, maxLength, longestShortName, avgLength, medianLength, p80Length, p90Length } = await app2.getShortNameStats();
|
|
558
|
+
console.log("uniqueCharacters:", uniqueCharacters);
|
|
559
|
+
console.log("maxLength:", maxLength);
|
|
560
|
+
console.log("longestShortName:", longestShortName);
|
|
561
|
+
console.log("avgLength:", avgLength);
|
|
562
|
+
console.log("medianLength:", medianLength);
|
|
563
|
+
console.log("p80Length:", p80Length);
|
|
564
|
+
console.log("p90Length:", p90Length);
|
|
565
|
+
} catch (error) {
|
|
566
|
+
if (error instanceof WGApiError) {
|
|
567
|
+
console.error(`API error [${error.code}] ${error.field}: ${error.message}`);
|
|
568
|
+
} else {
|
|
569
|
+
console.error("Error:", error instanceof Error ? error.message : error);
|
|
570
|
+
}
|
|
571
|
+
process.exit(1);
|
|
572
|
+
}
|
|
573
|
+
});
|
|
574
|
+
}
|
|
575
|
+
|
|
576
|
+
// src/commands/vehicle/long-aliases.ts
|
|
577
|
+
import { Command as Command7 } from "commander";
|
|
578
|
+
|
|
579
|
+
// src/lib/icons/pogs/short-names.ts
|
|
580
|
+
import { readFileSync, writeFileSync } from "fs";
|
|
581
|
+
import JSON5 from "json5";
|
|
582
|
+
|
|
583
|
+
// src/lib/pkg-root.ts
|
|
584
|
+
import { existsSync } from "fs";
|
|
585
|
+
import { dirname, resolve as resolve3 } from "path";
|
|
586
|
+
import { fileURLToPath } from "url";
|
|
587
|
+
function findPkgRoot(url) {
|
|
588
|
+
let d = dirname(fileURLToPath(url));
|
|
589
|
+
while (!existsSync(resolve3(d, "package.json"))) {
|
|
590
|
+
d = dirname(d);
|
|
591
|
+
}
|
|
592
|
+
return d;
|
|
593
|
+
}
|
|
594
|
+
function resolveAsset(url, ...parts) {
|
|
595
|
+
const adjacent = resolve3(dirname(fileURLToPath(url)), "assets", ...parts);
|
|
596
|
+
if (existsSync(adjacent)) {
|
|
597
|
+
return adjacent;
|
|
598
|
+
}
|
|
599
|
+
return resolve3(findPkgRoot(url), "assets", ...parts);
|
|
600
|
+
}
|
|
601
|
+
|
|
602
|
+
// src/lib/icons/pogs/short-names.ts
|
|
603
|
+
var shortNamesPath = resolveAsset(new URL(import.meta.url), "pogs", "short-names.json5");
|
|
604
|
+
var dict = {
|
|
605
|
+
...JSON5.parse(readFileSync(shortNamesPath, "utf8"))
|
|
606
|
+
};
|
|
607
|
+
function lookupShortName(vehicle2) {
|
|
608
|
+
return dict[vehicle2.tag] ?? vehicle2.short_name;
|
|
609
|
+
}
|
|
610
|
+
function isResolved(vehicle2) {
|
|
611
|
+
return vehicle2.tag in dict;
|
|
612
|
+
}
|
|
613
|
+
function add(vehicle2, alias) {
|
|
614
|
+
dict[vehicle2.tag] = alias;
|
|
615
|
+
}
|
|
616
|
+
function save() {
|
|
617
|
+
const sorted = Object.fromEntries(
|
|
618
|
+
Object.entries(dict).sort(([, a], [, b]) => b.length - a.length)
|
|
619
|
+
);
|
|
620
|
+
writeFileSync(shortNamesPath, JSON.stringify(sorted, null, 2) + "\n", "utf8");
|
|
621
|
+
}
|
|
622
|
+
|
|
623
|
+
// src/commands/vehicle/long-aliases.ts
|
|
624
|
+
function longAliasesCommand(app2) {
|
|
625
|
+
return new Command7("long-aliases").description("List vehicles whose shortened alias exceeds 10 characters").option("--update", "back-fill dictionary with aliases that are too long or unresolved").action(async (options) => {
|
|
626
|
+
try {
|
|
627
|
+
const vehicles = await app2.getVehicles();
|
|
628
|
+
let dirty = false;
|
|
629
|
+
for (const vehicle2 of vehicles) {
|
|
630
|
+
const alias = lookupShortName(vehicle2);
|
|
631
|
+
const tooLong = alias.length > 10;
|
|
632
|
+
const unresolved = !isResolved(vehicle2);
|
|
633
|
+
if (tooLong || unresolved) {
|
|
634
|
+
console.log(`${vehicle2.tank_id} ${vehicle2.tag} "${alias}"`);
|
|
635
|
+
if (options.update) {
|
|
636
|
+
add(vehicle2, alias);
|
|
637
|
+
dirty = true;
|
|
638
|
+
}
|
|
639
|
+
}
|
|
640
|
+
}
|
|
641
|
+
if (dirty) {
|
|
642
|
+
save();
|
|
643
|
+
}
|
|
644
|
+
} catch (error) {
|
|
645
|
+
if (error instanceof WGApiError) {
|
|
646
|
+
console.error(`API error [${error.code}] ${error.field}: ${error.message}`);
|
|
647
|
+
} else {
|
|
648
|
+
console.error("Error:", error instanceof Error ? error.message : error);
|
|
649
|
+
}
|
|
650
|
+
process.exit(1);
|
|
651
|
+
}
|
|
652
|
+
});
|
|
653
|
+
}
|
|
654
|
+
|
|
655
|
+
// src/commands/font/render.ts
|
|
656
|
+
import { Command as Command8 } from "commander";
|
|
657
|
+
import sharp3 from "sharp";
|
|
658
|
+
|
|
659
|
+
// src/lib/fonts/pogs-numbers.ts
|
|
660
|
+
import { readFileSync as readFileSync2 } from "fs";
|
|
661
|
+
import JSON52 from "json5";
|
|
662
|
+
var pogsNumbers = JSON52.parse(
|
|
663
|
+
readFileSync2(resolveAsset(new URL(import.meta.url), "fonts", "pogs", "numbers.json5"), "utf8")
|
|
664
|
+
);
|
|
665
|
+
|
|
666
|
+
// src/lib/fonts/pogs-numbers-bold.ts
|
|
667
|
+
import { readFileSync as readFileSync3 } from "fs";
|
|
668
|
+
import JSON53 from "json5";
|
|
669
|
+
var pogsNumbersBold = JSON53.parse(
|
|
670
|
+
readFileSync3(resolveAsset(new URL(import.meta.url), "fonts", "pogs", "numbers-bold.json5"), "utf8")
|
|
671
|
+
);
|
|
672
|
+
|
|
673
|
+
// src/lib/fonts/pogs-4px.ts
|
|
674
|
+
import { readFileSync as readFileSync4 } from "fs";
|
|
675
|
+
import JSON54 from "json5";
|
|
676
|
+
var pogs4px = JSON54.parse(
|
|
677
|
+
readFileSync4(resolveAsset(new URL(import.meta.url), "fonts", "pogs", "4px.json5"), "utf8")
|
|
678
|
+
);
|
|
679
|
+
|
|
680
|
+
// src/lib/fonts/pogs-3px.ts
|
|
681
|
+
import { readFileSync as readFileSync5 } from "fs";
|
|
682
|
+
import JSON55 from "json5";
|
|
683
|
+
var pogs3px = JSON55.parse(
|
|
684
|
+
readFileSync5(resolveAsset(new URL(import.meta.url), "fonts", "pogs", "3px.json5"), "utf8")
|
|
685
|
+
);
|
|
686
|
+
|
|
687
|
+
// src/lib/fonts/index.ts
|
|
688
|
+
var fonts = {
|
|
689
|
+
pogsNumbers,
|
|
690
|
+
pogsNumbersBold,
|
|
691
|
+
pogs4px,
|
|
692
|
+
pogs3px
|
|
693
|
+
};
|
|
694
|
+
|
|
695
|
+
// src/lib/render-text.ts
|
|
696
|
+
import sharp2 from "sharp";
|
|
697
|
+
|
|
698
|
+
// src/lib/fonts/glyph.ts
|
|
699
|
+
function charToPixel(ch) {
|
|
700
|
+
const c = ch.toLowerCase().trim();
|
|
701
|
+
switch (c) {
|
|
702
|
+
case "-":
|
|
703
|
+
return "quarter";
|
|
704
|
+
case "+":
|
|
705
|
+
return "half";
|
|
706
|
+
case "*":
|
|
707
|
+
case "=":
|
|
708
|
+
return "three-quarter";
|
|
709
|
+
}
|
|
710
|
+
if (c === "#" || /^[a-z]$/.test(c)) {
|
|
711
|
+
return "full";
|
|
712
|
+
}
|
|
713
|
+
return "empty";
|
|
714
|
+
}
|
|
715
|
+
function normalizeGlyph(rawRows, noTrim = false) {
|
|
716
|
+
const trimmed = noTrim ? rawRows : rawRows.map((row) => row.replace(/\s+$/u, ""));
|
|
717
|
+
const width = trimmed.reduce((max, row) => Math.max(max, row.length), 0);
|
|
718
|
+
const rows = trimmed.map((row) => {
|
|
719
|
+
const padded = row.padEnd(width, " ");
|
|
720
|
+
return Array.from(padded, charToPixel);
|
|
721
|
+
});
|
|
722
|
+
return { width, height: rows.length, rows };
|
|
723
|
+
}
|
|
724
|
+
|
|
725
|
+
// src/lib/PixelFont.ts
|
|
726
|
+
var PIXEL_KIND_ALPHA = {
|
|
727
|
+
empty: 0,
|
|
728
|
+
quarter: 64,
|
|
729
|
+
half: 128,
|
|
730
|
+
"three-quarter": 191,
|
|
731
|
+
full: 255
|
|
732
|
+
};
|
|
733
|
+
var PixelFont = class {
|
|
734
|
+
glyphMap;
|
|
735
|
+
pixelCache = /* @__PURE__ */ new Map();
|
|
736
|
+
constructor(font2) {
|
|
737
|
+
this.glyphMap = new Map(
|
|
738
|
+
Object.entries(font2.glyphs).map(([ch, rows]) => {
|
|
739
|
+
return [ch, normalizeGlyph(rows, ch === " ")];
|
|
740
|
+
})
|
|
741
|
+
);
|
|
742
|
+
}
|
|
743
|
+
getPixels(char) {
|
|
744
|
+
let result = this.pixelCache.get(char);
|
|
745
|
+
if (result === void 0) {
|
|
746
|
+
const glyph = this.glyphMap.get(char) ?? null;
|
|
747
|
+
result = glyph === null ? null : { width: glyph.width, height: glyph.height, alpha: glyph.rows.map((row) => row.map((k) => PIXEL_KIND_ALPHA[k])) };
|
|
748
|
+
this.pixelCache.set(char, result);
|
|
749
|
+
}
|
|
750
|
+
return result;
|
|
751
|
+
}
|
|
752
|
+
};
|
|
753
|
+
|
|
754
|
+
// src/lib/colors.ts
|
|
755
|
+
var Colors = {
|
|
756
|
+
white: 4294967295,
|
|
757
|
+
black: 255,
|
|
758
|
+
beige: 4172968703,
|
|
759
|
+
yellow: 3956737535
|
|
760
|
+
};
|
|
761
|
+
|
|
762
|
+
// src/lib/render-text.ts
|
|
763
|
+
var CHAR_GAP = 1;
|
|
764
|
+
var prepareGlyphsCache = /* @__PURE__ */ new Map();
|
|
765
|
+
function prepareGlyphs(fontName, text) {
|
|
766
|
+
const str = String(text);
|
|
767
|
+
const key = `${fontName}:${str}`;
|
|
768
|
+
let result = prepareGlyphsCache.get(key);
|
|
769
|
+
if (result === void 0) {
|
|
770
|
+
const fontDef = fonts[fontName];
|
|
771
|
+
if (!fontDef) {
|
|
772
|
+
throw new Error(`Unknown font: "${fontName}". Available: ${Object.keys(fonts).join(", ")}`);
|
|
773
|
+
}
|
|
774
|
+
const pf = new PixelFont(fontDef);
|
|
775
|
+
const chars = [...str];
|
|
776
|
+
const charPixels = chars.map((ch) => pf.getPixels(ch));
|
|
777
|
+
const charWidths = charPixels.map((p) => p?.width ?? 0);
|
|
778
|
+
const width = charWidths.reduce((s, w) => s + w, 0) + CHAR_GAP * Math.max(0, chars.length - 1);
|
|
779
|
+
const height = chars.length > 0 ? Math.max(...charPixels.map((p) => p?.height ?? 0)) : 0;
|
|
780
|
+
result = { chars, charPixels, charWidths, width, height };
|
|
781
|
+
prepareGlyphsCache.set(key, result);
|
|
782
|
+
}
|
|
783
|
+
return result;
|
|
784
|
+
}
|
|
785
|
+
function renderText(fontName, text, color = Colors.white) {
|
|
786
|
+
const { chars, charPixels, charWidths, width, height } = prepareGlyphs(fontName, text);
|
|
787
|
+
const cr = color >>> 24 & 255;
|
|
788
|
+
const cg = color >>> 16 & 255;
|
|
789
|
+
const cb = color >>> 8 & 255;
|
|
790
|
+
const ca = color & 255;
|
|
791
|
+
const data = Buffer.alloc(width * height * 4, 0);
|
|
792
|
+
let x = 0;
|
|
793
|
+
for (let ci = 0; ci < chars.length; ci++) {
|
|
794
|
+
const pixels = charPixels[ci];
|
|
795
|
+
if (pixels) {
|
|
796
|
+
for (let row = 0; row < pixels.alpha.length; row++) {
|
|
797
|
+
for (let col = 0; col < pixels.alpha[row].length; col++) {
|
|
798
|
+
const alpha = pixels.alpha[row][col];
|
|
799
|
+
if (alpha === 0) {
|
|
800
|
+
continue;
|
|
801
|
+
}
|
|
802
|
+
const off = (row * width + (x + col)) * 4;
|
|
803
|
+
data[off] = cr;
|
|
804
|
+
data[off + 1] = cg;
|
|
805
|
+
data[off + 2] = cb;
|
|
806
|
+
data[off + 3] = Math.round(alpha / 255 * ca);
|
|
807
|
+
}
|
|
808
|
+
}
|
|
809
|
+
}
|
|
810
|
+
x += charWidths[ci] + CHAR_GAP;
|
|
811
|
+
}
|
|
812
|
+
return { data, width, height };
|
|
813
|
+
}
|
|
814
|
+
async function renderWithShadow(fontName, text, color = Colors.white, shadowColor = Colors.black, distance = 1) {
|
|
815
|
+
const main = renderText(fontName, text, color);
|
|
816
|
+
const shadow = renderText(fontName, text, shadowColor);
|
|
817
|
+
const width = main.width + distance;
|
|
818
|
+
const height = main.height + distance;
|
|
819
|
+
const base = Buffer.alloc(width * height * 4, 0);
|
|
820
|
+
const { data } = await sharp2(base, { raw: { width, height, channels: 4 } }).composite([
|
|
821
|
+
{ input: shadow.data, raw: { width: shadow.width, height: shadow.height, channels: 4 }, left: distance, top: distance },
|
|
822
|
+
{ input: main.data, raw: { width: main.width, height: main.height, channels: 4 }, left: 0, top: 0 }
|
|
823
|
+
]).raw().toBuffer({ resolveWithObject: true });
|
|
824
|
+
return { data, width, height };
|
|
825
|
+
}
|
|
826
|
+
|
|
827
|
+
// src/commands/font/render.ts
|
|
828
|
+
var DEFAULT_SAMPLE = "0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ";
|
|
829
|
+
var PADDING = 2;
|
|
830
|
+
function renderCommand() {
|
|
831
|
+
return new Command8("render").description("Render a font sample as a PNG image in CWD").argument("[font]", `font name (${Object.keys(fonts).join(", ")})`).argument("[text]", "text to render", DEFAULT_SAMPLE).action(async (fontName, text) => {
|
|
832
|
+
if (!fontName) {
|
|
833
|
+
console.log(Object.keys(fonts).join("\n"));
|
|
834
|
+
return;
|
|
835
|
+
}
|
|
836
|
+
if (!fonts[fontName]) {
|
|
837
|
+
console.error(`Unknown font: ${fontName}. Available: ${Object.keys(fonts).join(", ")}`);
|
|
838
|
+
process.exit(1);
|
|
839
|
+
}
|
|
840
|
+
const { data: textData, width: textW, height: textH } = renderText(fontName, text);
|
|
841
|
+
const totalW = PADDING * 2 + textW;
|
|
842
|
+
const totalH = PADDING * 2 + textH;
|
|
843
|
+
const buf = Buffer.alloc(totalW * totalH * 3, 255);
|
|
844
|
+
for (let row = 0; row < textH; row++) {
|
|
845
|
+
for (let col = 0; col < textW; col++) {
|
|
846
|
+
const alpha = textData[(row * textW + col) * 4 + 3];
|
|
847
|
+
if (alpha === 0) {
|
|
848
|
+
continue;
|
|
849
|
+
}
|
|
850
|
+
const grey = 255 - alpha;
|
|
851
|
+
const off = ((PADDING + row) * totalW + (PADDING + col)) * 3;
|
|
852
|
+
buf[off] = grey;
|
|
853
|
+
buf[off + 1] = grey;
|
|
854
|
+
buf[off + 2] = grey;
|
|
855
|
+
}
|
|
856
|
+
}
|
|
857
|
+
const outPath = `${fontName}.png`;
|
|
858
|
+
await sharp3(buf, { raw: { width: totalW, height: totalH, channels: 3 } }).png().toFile(outPath);
|
|
859
|
+
console.log(`${outPath} \u2014 ${[...text].length} chars, ${totalW}\xD7${totalH}px`);
|
|
860
|
+
});
|
|
861
|
+
}
|
|
862
|
+
|
|
863
|
+
// src/commands/atlas/pick.ts
|
|
864
|
+
import { Command as Command9 } from "commander";
|
|
865
|
+
import { writeFile as writeFile3 } from "fs/promises";
|
|
866
|
+
import { PNG } from "pngjs";
|
|
867
|
+
function pickCommand(atlas2) {
|
|
868
|
+
return new Command9("pick").description("Extract a named texture region from an atlas and save as PNG").argument("<atlas>", "path to atlas XML").argument("<image>", "path to atlas image (always read as PNG)").argument("<name>", "texture name to extract").action(async (atlasPath, imagePath, name) => {
|
|
869
|
+
const result = await atlas2.pick(atlasPath, imagePath, name);
|
|
870
|
+
if (!result) {
|
|
871
|
+
console.error(`Texture not found: ${name}`);
|
|
872
|
+
console.error(`Run 'pie-wot atlas inspect ${atlasPath}' to list available textures`);
|
|
873
|
+
process.exit(1);
|
|
874
|
+
}
|
|
875
|
+
const outPath = `${name}.png`;
|
|
876
|
+
await writeFile3(outPath, PNG.sync.write(result));
|
|
877
|
+
console.log(`${outPath} \u2014 ${result.width}\xD7${result.height}px`);
|
|
878
|
+
});
|
|
879
|
+
}
|
|
880
|
+
|
|
881
|
+
// src/commands/atlas/inspect.ts
|
|
882
|
+
import { Command as Command10 } from "commander";
|
|
883
|
+
function inspectAtlasCommand(atlas2) {
|
|
884
|
+
return new Command10("inspect").description("List all texture names in an atlas XML").argument("<atlas>", "path to atlas XML").action(async (atlasPath) => {
|
|
885
|
+
const names = await atlas2.listNames(atlasPath);
|
|
886
|
+
names.forEach((name) => console.log(name));
|
|
887
|
+
});
|
|
888
|
+
}
|
|
889
|
+
|
|
890
|
+
// src/commands/atlas/extract.ts
|
|
891
|
+
import { Command as Command11 } from "commander";
|
|
892
|
+
function extractAtlasCommand(atlas2) {
|
|
893
|
+
return new Command11("extract").description("Extract all textures from an atlas into separate PNG files").option("--from <path>", "base path \u2014 derives <path>.png (image) and <path>.xml (map)").option("--image <path>", "atlas image path (PNG)").option("--map <path>", "atlas XML map path").requiredOption("--to <dir>", "destination directory").action(async (options) => {
|
|
894
|
+
let imagePath;
|
|
895
|
+
let mapPath;
|
|
896
|
+
if (options.from) {
|
|
897
|
+
imagePath = `${options.from}.png`;
|
|
898
|
+
mapPath = `${options.from}.xml`;
|
|
899
|
+
} else if (options.image && options.map) {
|
|
900
|
+
imagePath = options.image;
|
|
901
|
+
mapPath = options.map;
|
|
902
|
+
} else {
|
|
903
|
+
console.error("Provide either --from <path> or both --image <path> and --map <path>");
|
|
904
|
+
process.exit(1);
|
|
905
|
+
}
|
|
906
|
+
const count = await atlas2.extractAll(mapPath, imagePath, options.to);
|
|
907
|
+
console.log(`Extracted ${count} textures \u2192 ${options.to}`);
|
|
908
|
+
});
|
|
909
|
+
}
|
|
910
|
+
|
|
911
|
+
// src/commands/atlas/pack.ts
|
|
912
|
+
import { Command as Command12 } from "commander";
|
|
913
|
+
import { writeFile as writeFile4 } from "fs/promises";
|
|
914
|
+
function packAtlasCommand(atlas2) {
|
|
915
|
+
return new Command12("pack").description("Pack a directory of PNGs into a texture atlas").option("--from <dir>", "source directory containing PNG images").option("--src <dir>", "alias for --from").option("--to <path>", "output base path (gets .png and .xml extensions)").option("--dst <path>", "alias for --to").option("--padding <n>", "pixels of padding between textures", "0").option("--max-width <n>", "maximum atlas width in pixels", "4096").action(
|
|
916
|
+
async (options) => {
|
|
917
|
+
const fromDir = options.from ?? options.src;
|
|
918
|
+
const toPath = options.to ?? options.dst;
|
|
919
|
+
if (!fromDir) {
|
|
920
|
+
console.error("Provide --from <dir> or --src <dir>");
|
|
921
|
+
process.exit(1);
|
|
922
|
+
}
|
|
923
|
+
if (!toPath) {
|
|
924
|
+
console.error("Provide --to <path> or --dst <path>");
|
|
925
|
+
process.exit(1);
|
|
926
|
+
}
|
|
927
|
+
const result = await atlas2.pack(fromDir, {
|
|
928
|
+
padding: parseInt(options.padding, 10),
|
|
929
|
+
maxWidth: parseInt(options.maxWidth, 10)
|
|
930
|
+
});
|
|
931
|
+
if (result.bins > 1) {
|
|
932
|
+
console.warn(`Warning: textures span ${result.bins} bins \u2014 only the first will be written`);
|
|
933
|
+
}
|
|
934
|
+
await writeFile4(`${toPath}.png`, result.pngBuffer);
|
|
935
|
+
await writeFile4(`${toPath}.xml`, result.xml);
|
|
936
|
+
console.log(`Packed ${result.count} textures \u2192 ${toPath}.png (${result.width}\xD7${result.height})`);
|
|
937
|
+
}
|
|
938
|
+
);
|
|
939
|
+
}
|
|
940
|
+
|
|
941
|
+
// src/lib/AtlasManager.ts
|
|
942
|
+
import { readFile as readFile3, readdir, writeFile as writeFile5, mkdir as mkdir2 } from "fs/promises";
|
|
943
|
+
import { join as join3, basename as basename2, extname } from "path";
|
|
944
|
+
import { PNG as PNG2 } from "pngjs";
|
|
945
|
+
import { MaxRectsPacker } from "maxrects-packer";
|
|
946
|
+
|
|
947
|
+
// src/lib/texture-atlas.ts
|
|
948
|
+
import { readFile as readFile2 } from "fs/promises";
|
|
949
|
+
import { XMLParser } from "fast-xml-parser";
|
|
950
|
+
var parser = new XMLParser();
|
|
951
|
+
async function readTextureAtlas(xmlPath) {
|
|
952
|
+
const xml = await readFile2(xmlPath, "utf-8");
|
|
953
|
+
const doc = parser.parse(xml);
|
|
954
|
+
const raw = doc.root?.SubTexture;
|
|
955
|
+
if (!raw) {
|
|
956
|
+
return [];
|
|
957
|
+
}
|
|
958
|
+
const entries = Array.isArray(raw) ? raw : [raw];
|
|
959
|
+
const regions = [];
|
|
960
|
+
for (const entry of entries) {
|
|
961
|
+
if (typeof entry !== "object" || entry === null) {
|
|
962
|
+
continue;
|
|
963
|
+
}
|
|
964
|
+
const e = entry;
|
|
965
|
+
const name = typeof e.name === "string" ? e.name.trim() : null;
|
|
966
|
+
const x = Number(e.x);
|
|
967
|
+
const y = Number(e.y);
|
|
968
|
+
const width = Number(e.width);
|
|
969
|
+
const height = Number(e.height);
|
|
970
|
+
if (name && !isNaN(x) && !isNaN(y) && !isNaN(width) && !isNaN(height)) {
|
|
971
|
+
regions.push({ name, x, y, width, height });
|
|
972
|
+
}
|
|
973
|
+
}
|
|
974
|
+
return regions;
|
|
975
|
+
}
|
|
976
|
+
|
|
977
|
+
// src/lib/AtlasManager.ts
|
|
978
|
+
function copyRegion(src, region) {
|
|
979
|
+
const out = new PNG2({ width: region.width, height: region.height });
|
|
980
|
+
for (let row = 0; row < region.height; row++) {
|
|
981
|
+
const srcOffset = ((region.y + row) * src.width + region.x) * 4;
|
|
982
|
+
const dstOffset = row * region.width * 4;
|
|
983
|
+
src.data.copy(out.data, dstOffset, srcOffset, srcOffset + region.width * 4);
|
|
984
|
+
}
|
|
985
|
+
return out;
|
|
986
|
+
}
|
|
987
|
+
function buildXml(regions) {
|
|
988
|
+
const entries = regions.map(
|
|
989
|
+
(r) => ` <SubTexture>
|
|
990
|
+
<name> ${r.name} </name>
|
|
991
|
+
<x> ${r.x} </x>
|
|
992
|
+
<y> ${r.y} </y>
|
|
993
|
+
<width> ${r.width} </width>
|
|
994
|
+
<height> ${r.height} </height>
|
|
995
|
+
</SubTexture>`
|
|
996
|
+
).join("\n");
|
|
997
|
+
return `<root>
|
|
998
|
+
${entries}
|
|
999
|
+
</root>
|
|
1000
|
+
`;
|
|
1001
|
+
}
|
|
1002
|
+
var AtlasManager = class {
|
|
1003
|
+
async listNames(mapPath) {
|
|
1004
|
+
const regions = await readTextureAtlas(mapPath);
|
|
1005
|
+
return regions.map((r) => r.name).sort();
|
|
1006
|
+
}
|
|
1007
|
+
async pick(mapPath, imagePath, name) {
|
|
1008
|
+
const regions = await readTextureAtlas(mapPath);
|
|
1009
|
+
const region = regions.find((r) => r.name === name);
|
|
1010
|
+
if (!region) {
|
|
1011
|
+
return null;
|
|
1012
|
+
}
|
|
1013
|
+
const src = PNG2.sync.read(await readFile3(imagePath));
|
|
1014
|
+
return copyRegion(src, region);
|
|
1015
|
+
}
|
|
1016
|
+
async extractAll(mapPath, imagePath, destDir) {
|
|
1017
|
+
const regions = await readTextureAtlas(mapPath);
|
|
1018
|
+
const src = PNG2.sync.read(await readFile3(imagePath));
|
|
1019
|
+
await mkdir2(destDir, { recursive: true });
|
|
1020
|
+
for (const region of regions) {
|
|
1021
|
+
const out = copyRegion(src, region);
|
|
1022
|
+
await writeFile5(join3(destDir, `${region.name}.png`), PNG2.sync.write(out));
|
|
1023
|
+
}
|
|
1024
|
+
return regions.length;
|
|
1025
|
+
}
|
|
1026
|
+
async pack(srcDir, { padding = 0, maxWidth = 4096 } = {}) {
|
|
1027
|
+
const files = (await readdir(srcDir)).filter((f) => extname(f).toLowerCase() === ".png").sort();
|
|
1028
|
+
if (files.length === 0) {
|
|
1029
|
+
throw new Error(`No PNG files found in ${srcDir}`);
|
|
1030
|
+
}
|
|
1031
|
+
const entries = await Promise.all(
|
|
1032
|
+
files.map(async (file) => {
|
|
1033
|
+
const png = PNG2.sync.read(await readFile3(join3(srcDir, file)));
|
|
1034
|
+
return { name: basename2(file, ".png"), png, x: 0, y: 0, width: png.width, height: png.height };
|
|
1035
|
+
})
|
|
1036
|
+
);
|
|
1037
|
+
const packer = new MaxRectsPacker(maxWidth, 32768, padding, {
|
|
1038
|
+
smart: true,
|
|
1039
|
+
pot: false,
|
|
1040
|
+
square: false,
|
|
1041
|
+
allowRotation: false
|
|
1042
|
+
});
|
|
1043
|
+
packer.addArray(entries);
|
|
1044
|
+
const bin = packer.bins[0];
|
|
1045
|
+
const out = new PNG2({ width: bin.width, height: bin.height });
|
|
1046
|
+
out.data.fill(0);
|
|
1047
|
+
const regions = [];
|
|
1048
|
+
for (const rect of bin.rects) {
|
|
1049
|
+
const { x, y, width, height, name, png } = rect;
|
|
1050
|
+
for (let row = 0; row < height; row++) {
|
|
1051
|
+
const srcOffset = row * width * 4;
|
|
1052
|
+
const dstOffset = ((y + row) * bin.width + x) * 4;
|
|
1053
|
+
png.data.copy(out.data, dstOffset, srcOffset, srcOffset + width * 4);
|
|
1054
|
+
}
|
|
1055
|
+
regions.push({ name, x, y, width, height });
|
|
1056
|
+
}
|
|
1057
|
+
regions.sort((a, b) => a.name.localeCompare(b.name));
|
|
1058
|
+
return {
|
|
1059
|
+
pngBuffer: PNG2.sync.write(out),
|
|
1060
|
+
xml: buildXml(regions),
|
|
1061
|
+
width: bin.width,
|
|
1062
|
+
height: bin.height,
|
|
1063
|
+
count: entries.length,
|
|
1064
|
+
bins: packer.bins.length
|
|
1065
|
+
};
|
|
1066
|
+
}
|
|
1067
|
+
};
|
|
1068
|
+
|
|
1069
|
+
// src/commands/dds/decode.ts
|
|
1070
|
+
import { readFile as readFile4, writeFile as writeFile6 } from "fs/promises";
|
|
1071
|
+
import { extname as extname2, join as join4, dirname as dirname2, basename as basename3 } from "path";
|
|
1072
|
+
import { Command as Command13 } from "commander";
|
|
1073
|
+
import { PNG as PNG3 } from "pngjs";
|
|
1074
|
+
|
|
1075
|
+
// src/lib/utex-mod/DDSUtils.ts
|
|
1076
|
+
var DDSUtils = class _DDSUtils {
|
|
1077
|
+
// ── DDS header flags ────────────────────────────────────────────────────────
|
|
1078
|
+
static DDSD_CAPS = 1;
|
|
1079
|
+
// always required
|
|
1080
|
+
static DDSD_HEIGHT = 2;
|
|
1081
|
+
// always required
|
|
1082
|
+
static DDSD_WIDTH = 4;
|
|
1083
|
+
// always required
|
|
1084
|
+
static DDSD_PIXELFORMAT = 4096;
|
|
1085
|
+
// always required
|
|
1086
|
+
static DDSD_MIPMAPCOUNT = 131072;
|
|
1087
|
+
static DDSD_LINEARSIZE = 524288;
|
|
1088
|
+
// ── DDS pixel format flags ──────────────────────────────────────────────────
|
|
1089
|
+
static DDPF_ALPHAPIXELS = 1;
|
|
1090
|
+
static DDPF_ALPHA = 2;
|
|
1091
|
+
static DDPF_FOURCC = 4;
|
|
1092
|
+
static DDPF_RGB = 64;
|
|
1093
|
+
static DDPF_LUMINANCE = 131072;
|
|
1094
|
+
// ── DDS caps flags ──────────────────────────────────────────────────────────
|
|
1095
|
+
static DDSCAPS_COMPLEX = 8;
|
|
1096
|
+
static DDSCAPS_MIPMAP = 4194304;
|
|
1097
|
+
static DDSCAPS_TEXTURE = 4096;
|
|
1098
|
+
// Reusable scratch buffers — allocated once per instance to avoid per-call
|
|
1099
|
+
// heap pressure in tight decode/encode loops. WARNING: contents are not
|
|
1100
|
+
// cleared between calls; never hold a reference to these across method
|
|
1101
|
+
// invocations or assume they are zero-initialised on entry.
|
|
1102
|
+
_int8 = new Uint8Array(4);
|
|
1103
|
+
_int = new Uint32Array(this._int8.buffer);
|
|
1104
|
+
// shares memory with _int8 for LE uint32 reinterpretation
|
|
1105
|
+
_arr16 = new Uint8Array(16);
|
|
1106
|
+
// 4-color palette slot for readBCcolor (16 RGBA entries × 1 byte)
|
|
1107
|
+
// ── public API ──────────────────────────────────────────────────────────────
|
|
1108
|
+
decode(buff) {
|
|
1109
|
+
var data = new Uint8Array(buff), offset = 4;
|
|
1110
|
+
var head = this.readHeader(data, offset);
|
|
1111
|
+
offset += 124;
|
|
1112
|
+
var pf = head.pixFormat;
|
|
1113
|
+
if (pf.flags & _DDSUtils.DDPF_FOURCC && pf.fourCC == "DX10") {
|
|
1114
|
+
offset += 20;
|
|
1115
|
+
}
|
|
1116
|
+
var w = head.width, h = head.height, out = [];
|
|
1117
|
+
var fmt = pf.fourCC;
|
|
1118
|
+
var mcnt = Math.max(1, head.mmcount);
|
|
1119
|
+
for (var it = 0; it < mcnt; it++) {
|
|
1120
|
+
var img = new Uint8Array(w * h * 4);
|
|
1121
|
+
if (fmt == "DXT1") {
|
|
1122
|
+
offset = this.readBC1(data, offset, img, w, h);
|
|
1123
|
+
} else if (fmt == "DXT3") {
|
|
1124
|
+
offset = this.readBC2(data, offset, img, w, h);
|
|
1125
|
+
} else if (fmt == "DXT5") {
|
|
1126
|
+
offset = this.readBC3(data, offset, img, w, h);
|
|
1127
|
+
} else if (fmt == "DX10") {
|
|
1128
|
+
throw new Error("Not supported: BC7 (DX10)");
|
|
1129
|
+
} else if (fmt == "ATC ") {
|
|
1130
|
+
throw new Error("Not supported: ATC");
|
|
1131
|
+
} else if (fmt == "ATCA") {
|
|
1132
|
+
throw new Error("Not supported: ATCA");
|
|
1133
|
+
} else if (fmt == "ATCI") {
|
|
1134
|
+
throw new Error("Not supported: ATCI");
|
|
1135
|
+
} else if (pf.flags & _DDSUtils.DDPF_ALPHAPIXELS && pf.flags & _DDSUtils.DDPF_RGB) {
|
|
1136
|
+
throw new Error("Not supported: (complex-A)");
|
|
1137
|
+
} else if (pf.flags & _DDSUtils.DDPF_ALPHA || pf.flags & _DDSUtils.DDPF_ALPHAPIXELS || pf.flags & _DDSUtils.DDPF_LUMINANCE) {
|
|
1138
|
+
throw new Error("Not supported: (complex-B)");
|
|
1139
|
+
} else {
|
|
1140
|
+
throw new Error(
|
|
1141
|
+
`Unknown texture format \u2014 head flags: ${head.flags.toString(2)}, pixelFormat flags: ${pf.flags.toString(2)}`
|
|
1142
|
+
);
|
|
1143
|
+
}
|
|
1144
|
+
out.push({ width: w, height: h, image: img.buffer });
|
|
1145
|
+
w = w >> 1;
|
|
1146
|
+
h = h >> 1;
|
|
1147
|
+
}
|
|
1148
|
+
return out;
|
|
1149
|
+
}
|
|
1150
|
+
encode(img, w, h, forceAlpha = true) {
|
|
1151
|
+
var imageAsByteArray = new Uint8Array(img);
|
|
1152
|
+
var aAnd = 255;
|
|
1153
|
+
for (var i = 3; i < imageAsByteArray.length; i += 4) {
|
|
1154
|
+
aAnd &= imageAsByteArray[i];
|
|
1155
|
+
}
|
|
1156
|
+
var gotAlpha = forceAlpha || aAnd < 250;
|
|
1157
|
+
var data = new Uint8Array(124 + w * h * 2), offset = 0;
|
|
1158
|
+
this.writeASCII(data, offset, "DDS ");
|
|
1159
|
+
offset += 4;
|
|
1160
|
+
this.writeHeader(data, w, h, gotAlpha, offset);
|
|
1161
|
+
offset += 124;
|
|
1162
|
+
var mcnt = 0;
|
|
1163
|
+
while (w * h != 0) {
|
|
1164
|
+
if (gotAlpha) {
|
|
1165
|
+
offset = this.writeBC3(imageAsByteArray, w, h, data, offset);
|
|
1166
|
+
} else {
|
|
1167
|
+
offset = this.writeBC1(imageAsByteArray, w, h, data, offset);
|
|
1168
|
+
}
|
|
1169
|
+
imageAsByteArray = this.mipmapB(imageAsByteArray, w, h);
|
|
1170
|
+
w = w >> 1;
|
|
1171
|
+
h = h >> 1;
|
|
1172
|
+
mcnt++;
|
|
1173
|
+
}
|
|
1174
|
+
data[28] = mcnt;
|
|
1175
|
+
return data.buffer.slice(0, offset);
|
|
1176
|
+
}
|
|
1177
|
+
// ── DDS header I/O ──────────────────────────────────────────────────────────
|
|
1178
|
+
readHeader(data, offset) {
|
|
1179
|
+
var hd = {};
|
|
1180
|
+
offset += 4;
|
|
1181
|
+
hd.flags = this.readUintLE(data, offset);
|
|
1182
|
+
offset += 4;
|
|
1183
|
+
hd.height = this.readUintLE(data, offset);
|
|
1184
|
+
offset += 4;
|
|
1185
|
+
hd.width = this.readUintLE(data, offset);
|
|
1186
|
+
offset += 4;
|
|
1187
|
+
hd.pitch = this.readUintLE(data, offset);
|
|
1188
|
+
offset += 4;
|
|
1189
|
+
hd.depth = this.readUintLE(data, offset);
|
|
1190
|
+
offset += 4;
|
|
1191
|
+
hd.mmcount = this.readUintLE(data, offset);
|
|
1192
|
+
offset += 4;
|
|
1193
|
+
offset += 11 * 4;
|
|
1194
|
+
hd.pixFormat = this.readPixFormat(data, offset);
|
|
1195
|
+
offset += 32;
|
|
1196
|
+
hd.caps = this.readUintLE(data, offset);
|
|
1197
|
+
offset += 4;
|
|
1198
|
+
hd.caps2 = this.readUintLE(data, offset);
|
|
1199
|
+
offset += 4;
|
|
1200
|
+
hd.caps3 = this.readUintLE(data, offset);
|
|
1201
|
+
offset += 4;
|
|
1202
|
+
hd.caps4 = this.readUintLE(data, offset);
|
|
1203
|
+
return hd;
|
|
1204
|
+
}
|
|
1205
|
+
writeHeader(data, w, h, gotAlpha, offset) {
|
|
1206
|
+
var flgs = _DDSUtils.DDSD_CAPS | _DDSUtils.DDSD_HEIGHT | _DDSUtils.DDSD_WIDTH | _DDSUtils.DDSD_PIXELFORMAT;
|
|
1207
|
+
flgs |= _DDSUtils.DDSD_MIPMAPCOUNT | _DDSUtils.DDSD_LINEARSIZE;
|
|
1208
|
+
var caps = _DDSUtils.DDSCAPS_COMPLEX | _DDSUtils.DDSCAPS_MIPMAP | _DDSUtils.DDSCAPS_TEXTURE;
|
|
1209
|
+
var pitch = (w * h >> 1) * (gotAlpha ? 2 : 1), depth = gotAlpha ? 1 : 0;
|
|
1210
|
+
this.writeUintLE(data, offset, 124);
|
|
1211
|
+
offset += 4;
|
|
1212
|
+
this.writeUintLE(data, offset, flgs);
|
|
1213
|
+
offset += 4;
|
|
1214
|
+
this.writeUintLE(data, offset, h);
|
|
1215
|
+
offset += 4;
|
|
1216
|
+
this.writeUintLE(data, offset, w);
|
|
1217
|
+
offset += 4;
|
|
1218
|
+
this.writeUintLE(data, offset, pitch);
|
|
1219
|
+
offset += 4;
|
|
1220
|
+
this.writeUintLE(data, offset, depth);
|
|
1221
|
+
offset += 4;
|
|
1222
|
+
this.writeUintLE(data, offset, 10);
|
|
1223
|
+
offset += 4;
|
|
1224
|
+
offset += 11 * 4;
|
|
1225
|
+
this.writePixFormat(data, gotAlpha, offset);
|
|
1226
|
+
offset += 32;
|
|
1227
|
+
this.writeUintLE(data, offset, caps);
|
|
1228
|
+
}
|
|
1229
|
+
readPixFormat(data, offset) {
|
|
1230
|
+
var pf = {};
|
|
1231
|
+
offset += 4;
|
|
1232
|
+
pf.flags = this.readUintLE(data, offset);
|
|
1233
|
+
offset += 4;
|
|
1234
|
+
pf.fourCC = this.readASCII(data, offset, 4);
|
|
1235
|
+
offset += 4;
|
|
1236
|
+
pf.bitCount = this.readUintLE(data, offset);
|
|
1237
|
+
offset += 4;
|
|
1238
|
+
pf.RMask = this.readUintLE(data, offset);
|
|
1239
|
+
offset += 4;
|
|
1240
|
+
pf.GMask = this.readUintLE(data, offset);
|
|
1241
|
+
offset += 4;
|
|
1242
|
+
pf.BMask = this.readUintLE(data, offset);
|
|
1243
|
+
offset += 4;
|
|
1244
|
+
pf.AMask = this.readUintLE(data, offset);
|
|
1245
|
+
return pf;
|
|
1246
|
+
}
|
|
1247
|
+
writePixFormat(data, gotAlpha, offset) {
|
|
1248
|
+
var flgs = _DDSUtils.DDPF_FOURCC;
|
|
1249
|
+
this.writeUintLE(data, offset, 32);
|
|
1250
|
+
offset += 4;
|
|
1251
|
+
this.writeUintLE(data, offset, flgs);
|
|
1252
|
+
offset += 4;
|
|
1253
|
+
this.writeASCII(data, offset, gotAlpha ? "DXT5" : "DXT1");
|
|
1254
|
+
}
|
|
1255
|
+
// ── BC codec ────────────────────────────────────────────────────────────────
|
|
1256
|
+
readBC1(data, offset, img, w, h) {
|
|
1257
|
+
var sqr = new Uint8Array(4 * 4 * 4);
|
|
1258
|
+
for (var y = 0; y < h; y += 4) {
|
|
1259
|
+
for (var x = 0; x < w; x += 4) {
|
|
1260
|
+
this.readBCcolor(data, offset, sqr);
|
|
1261
|
+
this.write4x4(img, w, h, x, y, sqr);
|
|
1262
|
+
offset += 8;
|
|
1263
|
+
}
|
|
1264
|
+
}
|
|
1265
|
+
return offset;
|
|
1266
|
+
}
|
|
1267
|
+
writeBC1(img, w, h, data, offset) {
|
|
1268
|
+
var sqr = new Uint8Array(16 * 4);
|
|
1269
|
+
for (var y = 0; y < h; y += 4) {
|
|
1270
|
+
for (var x = 0; x < w; x += 4) {
|
|
1271
|
+
this.read4x4(img, w, h, x, y, sqr);
|
|
1272
|
+
this.writeBCcolor(data, offset, sqr);
|
|
1273
|
+
offset += 8;
|
|
1274
|
+
}
|
|
1275
|
+
}
|
|
1276
|
+
return offset;
|
|
1277
|
+
}
|
|
1278
|
+
readBC2(data, offset, img, w, h) {
|
|
1279
|
+
var pos = { boff: offset * 8 };
|
|
1280
|
+
var sqr = new Uint8Array(4 * 4 * 4);
|
|
1281
|
+
for (var y = 0; y < h; y += 4) {
|
|
1282
|
+
for (var x = 0; x < w; x += 4) {
|
|
1283
|
+
this.readBCcolor(data, offset + 8, sqr);
|
|
1284
|
+
for (var i = 0; i < 64; i += 4) {
|
|
1285
|
+
var code = this.readBits(data, pos, 4);
|
|
1286
|
+
sqr[i + 3] = 255 * (code / 15);
|
|
1287
|
+
}
|
|
1288
|
+
this.write4x4(img, w, h, x, y, sqr);
|
|
1289
|
+
offset += 16;
|
|
1290
|
+
pos.boff += 64;
|
|
1291
|
+
}
|
|
1292
|
+
}
|
|
1293
|
+
return offset;
|
|
1294
|
+
}
|
|
1295
|
+
inter8(a, b) {
|
|
1296
|
+
var al = [a, b];
|
|
1297
|
+
if (a > b) {
|
|
1298
|
+
al.push(
|
|
1299
|
+
6 / 7 * a + 1 / 7 * b,
|
|
1300
|
+
// bit code 010
|
|
1301
|
+
5 / 7 * a + 2 / 7 * b,
|
|
1302
|
+
// bit code 011
|
|
1303
|
+
4 / 7 * a + 3 / 7 * b,
|
|
1304
|
+
// bit code 100
|
|
1305
|
+
3 / 7 * a + 4 / 7 * b,
|
|
1306
|
+
// bit code 101
|
|
1307
|
+
2 / 7 * a + 5 / 7 * b,
|
|
1308
|
+
// bit code 110
|
|
1309
|
+
1 / 7 * a + 6 / 7 * b
|
|
1310
|
+
);
|
|
1311
|
+
} else {
|
|
1312
|
+
al.push(
|
|
1313
|
+
4 / 5 * a + 1 / 5 * b,
|
|
1314
|
+
// bit code 010
|
|
1315
|
+
3 / 5 * a + 2 / 5 * b,
|
|
1316
|
+
// bit code 011
|
|
1317
|
+
2 / 5 * a + 3 / 5 * b,
|
|
1318
|
+
// bit code 100
|
|
1319
|
+
1 / 5 * a + 4 / 5 * b,
|
|
1320
|
+
// bit code 101
|
|
1321
|
+
0,
|
|
1322
|
+
// bit code 110
|
|
1323
|
+
255
|
|
1324
|
+
);
|
|
1325
|
+
}
|
|
1326
|
+
return al;
|
|
1327
|
+
}
|
|
1328
|
+
readBC3(data, offset, img, w, h) {
|
|
1329
|
+
var pos = { boff: offset * 8 };
|
|
1330
|
+
var sqr = new Uint8Array(4 * 4 * 4);
|
|
1331
|
+
for (var y = 0; y < h; y += 4) {
|
|
1332
|
+
for (var x = 0; x < w; x += 4) {
|
|
1333
|
+
this.readBCcolor(data, offset + 8, sqr);
|
|
1334
|
+
var al = this.inter8(data[offset], data[offset + 1]);
|
|
1335
|
+
pos.boff += 16;
|
|
1336
|
+
for (var i = 0; i < 64; i += 4) {
|
|
1337
|
+
var code = this.readBits(data, pos, 3);
|
|
1338
|
+
sqr[i + 3] = al[code];
|
|
1339
|
+
}
|
|
1340
|
+
pos.boff += 64;
|
|
1341
|
+
this.write4x4(img, w, h, x, y, sqr);
|
|
1342
|
+
offset += 16;
|
|
1343
|
+
}
|
|
1344
|
+
}
|
|
1345
|
+
return offset;
|
|
1346
|
+
}
|
|
1347
|
+
writeBC3(img, w, h, data, offset) {
|
|
1348
|
+
var sqr = new Uint8Array(16 * 4);
|
|
1349
|
+
for (var y = 0; y < h; y += 4) {
|
|
1350
|
+
for (var x = 0; x < w; x += 4) {
|
|
1351
|
+
this.read4x4(img, w, h, x, y, sqr);
|
|
1352
|
+
var min = sqr[3], max = sqr[3];
|
|
1353
|
+
for (var i = 7; i < 64; i += 4) {
|
|
1354
|
+
var a = sqr[i];
|
|
1355
|
+
if (a < min) {
|
|
1356
|
+
min = a;
|
|
1357
|
+
} else if (max < a) {
|
|
1358
|
+
max = a;
|
|
1359
|
+
}
|
|
1360
|
+
}
|
|
1361
|
+
data[offset] = max;
|
|
1362
|
+
data[offset + 1] = min;
|
|
1363
|
+
offset += 2;
|
|
1364
|
+
var al = this.inter8(max, min);
|
|
1365
|
+
for (var i = 0; i < 64; i += 32) {
|
|
1366
|
+
var bits = 0, boff = 0;
|
|
1367
|
+
for (var j = 0; j < 32; j += 4) {
|
|
1368
|
+
var code = 0, cd = 500;
|
|
1369
|
+
var a = sqr[i + j + 3];
|
|
1370
|
+
for (var k = 0; k < 8; k++) {
|
|
1371
|
+
var dst = Math.abs(al[k] - a);
|
|
1372
|
+
if (dst < cd) {
|
|
1373
|
+
cd = dst;
|
|
1374
|
+
code = k;
|
|
1375
|
+
}
|
|
1376
|
+
}
|
|
1377
|
+
bits = bits | code << boff;
|
|
1378
|
+
boff += 3;
|
|
1379
|
+
}
|
|
1380
|
+
data[offset] = bits;
|
|
1381
|
+
data[offset + 1] = bits >> 8;
|
|
1382
|
+
data[offset + 2] = bits >> 16;
|
|
1383
|
+
offset += 3;
|
|
1384
|
+
}
|
|
1385
|
+
this.writeBCcolor(data, offset, sqr);
|
|
1386
|
+
offset += 8;
|
|
1387
|
+
}
|
|
1388
|
+
}
|
|
1389
|
+
return offset;
|
|
1390
|
+
}
|
|
1391
|
+
readBCcolor(data, offset, sqr) {
|
|
1392
|
+
var c0 = data[offset + 1] << 8 | data[offset];
|
|
1393
|
+
var c1 = data[offset + 3] << 8 | data[offset + 2];
|
|
1394
|
+
var c0b = (c0 & 31) * (255 / 31), c0g = (c0 >>> 5 & 63) * (255 / 63), c0r = (c0 >> 11) * (255 / 31);
|
|
1395
|
+
var c1b = (c1 & 31) * (255 / 31), c1g = (c1 >>> 5 & 63) * (255 / 63), c1r = (c1 >> 11) * (255 / 31);
|
|
1396
|
+
var clr = this._arr16;
|
|
1397
|
+
clr[0] = ~~c0r;
|
|
1398
|
+
clr[1] = ~~c0g;
|
|
1399
|
+
clr[2] = ~~c0b;
|
|
1400
|
+
clr[3] = 255;
|
|
1401
|
+
clr[4] = ~~c1r;
|
|
1402
|
+
clr[5] = ~~c1g;
|
|
1403
|
+
clr[6] = ~~c1b;
|
|
1404
|
+
clr[7] = 255;
|
|
1405
|
+
if (c1 < c0) {
|
|
1406
|
+
var fr = 2 / 3, ifr = 1 - fr;
|
|
1407
|
+
clr[8] = ~~(fr * c0r + ifr * c1r);
|
|
1408
|
+
clr[9] = ~~(fr * c0g + ifr * c1g);
|
|
1409
|
+
clr[10] = ~~(fr * c0b + ifr * c1b);
|
|
1410
|
+
clr[11] = 255;
|
|
1411
|
+
fr = 1 / 3;
|
|
1412
|
+
ifr = 1 - fr;
|
|
1413
|
+
clr[12] = ~~(fr * c0r + ifr * c1r);
|
|
1414
|
+
clr[13] = ~~(fr * c0g + ifr * c1g);
|
|
1415
|
+
clr[14] = ~~(fr * c0b + ifr * c1b);
|
|
1416
|
+
clr[15] = 255;
|
|
1417
|
+
} else {
|
|
1418
|
+
var fr = 1 / 2, ifr = 1 - fr;
|
|
1419
|
+
clr[8] = ~~(fr * c0r + ifr * c1r);
|
|
1420
|
+
clr[9] = ~~(fr * c0g + ifr * c1g);
|
|
1421
|
+
clr[10] = ~~(fr * c0b + ifr * c1b);
|
|
1422
|
+
clr[11] = 255;
|
|
1423
|
+
clr[12] = 0;
|
|
1424
|
+
clr[13] = 0;
|
|
1425
|
+
clr[14] = 0;
|
|
1426
|
+
clr[15] = 0;
|
|
1427
|
+
}
|
|
1428
|
+
this.toSquare(data, sqr, clr, offset);
|
|
1429
|
+
}
|
|
1430
|
+
writeBCcolor(data, offset, sqr) {
|
|
1431
|
+
var ends = this.mostDistant(sqr);
|
|
1432
|
+
var c0r = sqr[ends >> 8], c0g = sqr[(ends >> 8) + 1], c0b = sqr[(ends >> 8) + 2];
|
|
1433
|
+
var c1r = sqr[ends & 255], c1g = sqr[(ends & 255) + 1], c1b = sqr[(ends & 255) + 2];
|
|
1434
|
+
var c0 = c0r >> 3 << 11 | c0g >> 2 << 5 | c0b >> 3;
|
|
1435
|
+
var c1 = c1r >> 3 << 11 | c1g >> 2 << 5 | c1b >> 3;
|
|
1436
|
+
if (c0 < c1) {
|
|
1437
|
+
var t = c0;
|
|
1438
|
+
c0 = c1;
|
|
1439
|
+
c1 = t;
|
|
1440
|
+
}
|
|
1441
|
+
var c0b = Math.floor((c0 & 31) * (255 / 31)), c0g = Math.floor((c0 >>> 5 & 63) * (255 / 63)), c0r = Math.floor((c0 >> 11) * (255 / 31));
|
|
1442
|
+
var c1b = Math.floor((c1 & 31) * (255 / 31)), c1g = Math.floor((c1 >>> 5 & 63) * (255 / 63)), c1r = Math.floor((c1 >> 11) * (255 / 31));
|
|
1443
|
+
data[offset + 0] = c0 & 255;
|
|
1444
|
+
data[offset + 1] = c0 >> 8;
|
|
1445
|
+
data[offset + 2] = c1 & 255;
|
|
1446
|
+
data[offset + 3] = c1 >> 8;
|
|
1447
|
+
var fr = 2 / 3, ifr = 1 - fr;
|
|
1448
|
+
var c2r = Math.floor(fr * c0r + ifr * c1r), c2g = Math.floor(fr * c0g + ifr * c1g), c2b = Math.floor(fr * c0b + ifr * c1b);
|
|
1449
|
+
fr = 1 / 3;
|
|
1450
|
+
ifr = 1 - fr;
|
|
1451
|
+
var c3r = Math.floor(fr * c0r + ifr * c1r), c3g = Math.floor(fr * c0g + ifr * c1g), c3b = Math.floor(fr * c0b + ifr * c1b);
|
|
1452
|
+
var boff = offset * 8 + 32;
|
|
1453
|
+
for (var i = 0; i < 64; i += 4) {
|
|
1454
|
+
var r = sqr[i], g = sqr[i + 1], b = sqr[i + 2];
|
|
1455
|
+
var ds0 = this.colorDist(r, g, b, c0r, c0g, c0b);
|
|
1456
|
+
var ds1 = this.colorDist(r, g, b, c1r, c1g, c1b);
|
|
1457
|
+
var ds2 = this.colorDist(r, g, b, c2r, c2g, c2b);
|
|
1458
|
+
var ds3 = this.colorDist(r, g, b, c3r, c3g, c3b);
|
|
1459
|
+
var dsm = Math.min(ds0, Math.min(ds1, Math.min(ds2, ds3)));
|
|
1460
|
+
var code = 0;
|
|
1461
|
+
if (dsm == ds1) {
|
|
1462
|
+
code = 1;
|
|
1463
|
+
} else if (dsm == ds2) {
|
|
1464
|
+
code = 2;
|
|
1465
|
+
} else if (dsm == ds3) {
|
|
1466
|
+
code = 3;
|
|
1467
|
+
}
|
|
1468
|
+
data[boff >> 3] |= code << (boff & 7);
|
|
1469
|
+
boff += 2;
|
|
1470
|
+
}
|
|
1471
|
+
}
|
|
1472
|
+
toSquare(data, sqr, clr, offset) {
|
|
1473
|
+
var boff = offset + 4 << 3;
|
|
1474
|
+
for (var i = 0; i < 64; i += 4) {
|
|
1475
|
+
var code = data[boff >> 3] >> (boff & 7) & 3;
|
|
1476
|
+
boff += 2;
|
|
1477
|
+
code = code << 2;
|
|
1478
|
+
sqr[i] = clr[code];
|
|
1479
|
+
sqr[i + 1] = clr[code + 1];
|
|
1480
|
+
sqr[i + 2] = clr[code + 2];
|
|
1481
|
+
sqr[i + 3] = clr[code + 3];
|
|
1482
|
+
}
|
|
1483
|
+
}
|
|
1484
|
+
read4x4(a, w, h, sx, sy, b) {
|
|
1485
|
+
for (var y = 0; y < 4; y++) {
|
|
1486
|
+
var si = (sy + y) * w + sx << 2, ti = y << 4;
|
|
1487
|
+
b[ti + 0] = a[si + 0];
|
|
1488
|
+
b[ti + 1] = a[si + 1];
|
|
1489
|
+
b[ti + 2] = a[si + 2];
|
|
1490
|
+
b[ti + 3] = a[si + 3];
|
|
1491
|
+
b[ti + 4] = a[si + 4];
|
|
1492
|
+
b[ti + 5] = a[si + 5];
|
|
1493
|
+
b[ti + 6] = a[si + 6];
|
|
1494
|
+
b[ti + 7] = a[si + 7];
|
|
1495
|
+
b[ti + 8] = a[si + 8];
|
|
1496
|
+
b[ti + 9] = a[si + 9];
|
|
1497
|
+
b[ti + 10] = a[si + 10];
|
|
1498
|
+
b[ti + 11] = a[si + 11];
|
|
1499
|
+
b[ti + 12] = a[si + 12];
|
|
1500
|
+
b[ti + 13] = a[si + 13];
|
|
1501
|
+
b[ti + 14] = a[si + 14];
|
|
1502
|
+
b[ti + 15] = a[si + 15];
|
|
1503
|
+
}
|
|
1504
|
+
}
|
|
1505
|
+
write4x4(a, w, h, sx, sy, b) {
|
|
1506
|
+
for (var y = 0; y < 4; y++) {
|
|
1507
|
+
var si = (sy + y) * w + sx << 2, ti = y << 4;
|
|
1508
|
+
a[si + 0] = b[ti + 0];
|
|
1509
|
+
a[si + 1] = b[ti + 1];
|
|
1510
|
+
a[si + 2] = b[ti + 2];
|
|
1511
|
+
a[si + 3] = b[ti + 3];
|
|
1512
|
+
a[si + 4] = b[ti + 4];
|
|
1513
|
+
a[si + 5] = b[ti + 5];
|
|
1514
|
+
a[si + 6] = b[ti + 6];
|
|
1515
|
+
a[si + 7] = b[ti + 7];
|
|
1516
|
+
a[si + 8] = b[ti + 8];
|
|
1517
|
+
a[si + 9] = b[ti + 9];
|
|
1518
|
+
a[si + 10] = b[ti + 10];
|
|
1519
|
+
a[si + 11] = b[ti + 11];
|
|
1520
|
+
a[si + 12] = b[ti + 12];
|
|
1521
|
+
a[si + 13] = b[ti + 13];
|
|
1522
|
+
a[si + 14] = b[ti + 14];
|
|
1523
|
+
a[si + 15] = b[ti + 15];
|
|
1524
|
+
}
|
|
1525
|
+
}
|
|
1526
|
+
rotate(sqr, rot) {
|
|
1527
|
+
if (rot == 0) {
|
|
1528
|
+
return;
|
|
1529
|
+
}
|
|
1530
|
+
for (var i = 0; i < 64; i += 4) {
|
|
1531
|
+
var r = sqr[i];
|
|
1532
|
+
var g = sqr[i + 1];
|
|
1533
|
+
var b = sqr[i + 2];
|
|
1534
|
+
var a = sqr[i + 3];
|
|
1535
|
+
if (rot == 1) {
|
|
1536
|
+
var t = a;
|
|
1537
|
+
a = r;
|
|
1538
|
+
r = t;
|
|
1539
|
+
}
|
|
1540
|
+
if (rot == 2) {
|
|
1541
|
+
var t = a;
|
|
1542
|
+
a = g;
|
|
1543
|
+
g = t;
|
|
1544
|
+
}
|
|
1545
|
+
if (rot == 3) {
|
|
1546
|
+
var t = a;
|
|
1547
|
+
a = b;
|
|
1548
|
+
b = t;
|
|
1549
|
+
}
|
|
1550
|
+
sqr[i] = r;
|
|
1551
|
+
sqr[i + 1] = g;
|
|
1552
|
+
sqr[i + 2] = b;
|
|
1553
|
+
sqr[i + 3] = a;
|
|
1554
|
+
}
|
|
1555
|
+
}
|
|
1556
|
+
readBits(data, pos, k) {
|
|
1557
|
+
var out = 0, ok = k;
|
|
1558
|
+
while (k != 0) {
|
|
1559
|
+
out = out | this.readBit(data, pos) << ok - k;
|
|
1560
|
+
k--;
|
|
1561
|
+
}
|
|
1562
|
+
return out;
|
|
1563
|
+
}
|
|
1564
|
+
readBit(data, pos) {
|
|
1565
|
+
var boff = pos.boff;
|
|
1566
|
+
pos.boff++;
|
|
1567
|
+
return data[boff >> 3] >> (boff & 7) & 1;
|
|
1568
|
+
}
|
|
1569
|
+
mipmapB(buff, w, h) {
|
|
1570
|
+
var nw = w >> 1, nh = h >> 1;
|
|
1571
|
+
var nbuf = new Uint8Array(nw * nh * 4);
|
|
1572
|
+
for (var y = 0; y < nh; y++) {
|
|
1573
|
+
for (var x = 0; x < nw; x++) {
|
|
1574
|
+
var ti = y * nw + x << 2, si = (y << 1) * w + (x << 1) << 2;
|
|
1575
|
+
var a0 = buff[si + 3], a1 = buff[si + 7];
|
|
1576
|
+
var r = buff[si] * a0 + buff[si + 4] * a1;
|
|
1577
|
+
var g = buff[si + 1] * a0 + buff[si + 5] * a1;
|
|
1578
|
+
var b = buff[si + 2] * a0 + buff[si + 6] * a1;
|
|
1579
|
+
si += w << 2;
|
|
1580
|
+
var a2 = buff[si + 3], a3 = buff[si + 7];
|
|
1581
|
+
r += buff[si] * a2 + buff[si + 4] * a3;
|
|
1582
|
+
g += buff[si + 1] * a2 + buff[si + 5] * a3;
|
|
1583
|
+
b += buff[si + 2] * a2 + buff[si + 6] * a3;
|
|
1584
|
+
var a = a0 + a1 + a2 + a3 + 2 >> 2, ia = a == 0 ? 0 : 0.25 / a;
|
|
1585
|
+
nbuf[ti] = ~~(r * ia + 0.5);
|
|
1586
|
+
nbuf[ti + 1] = ~~(g * ia + 0.5);
|
|
1587
|
+
nbuf[ti + 2] = ~~(b * ia + 0.5);
|
|
1588
|
+
nbuf[ti + 3] = a;
|
|
1589
|
+
}
|
|
1590
|
+
}
|
|
1591
|
+
return nbuf;
|
|
1592
|
+
}
|
|
1593
|
+
colorDist(r, g, b, r0, g0, b0) {
|
|
1594
|
+
return (r - r0) * (r - r0) + (g - g0) * (g - g0) + (b - b0) * (b - b0);
|
|
1595
|
+
}
|
|
1596
|
+
mostDistant(sqr) {
|
|
1597
|
+
var ends = 0, dd = 0;
|
|
1598
|
+
for (var i = 0; i < 64; i += 4) {
|
|
1599
|
+
var r = sqr[i], g = sqr[i + 1], b = sqr[i + 2];
|
|
1600
|
+
for (var j = i + 4; j < 64; j += 4) {
|
|
1601
|
+
var dst = this.colorDist(r, g, b, sqr[j], sqr[j + 1], sqr[j + 2]);
|
|
1602
|
+
if (dst > dd) {
|
|
1603
|
+
dd = dst;
|
|
1604
|
+
ends = i << 8 | j;
|
|
1605
|
+
}
|
|
1606
|
+
}
|
|
1607
|
+
}
|
|
1608
|
+
return ends;
|
|
1609
|
+
}
|
|
1610
|
+
// ── I/O helpers ─────────────────────────────────────────────────────────────
|
|
1611
|
+
readUintLE(buff, p) {
|
|
1612
|
+
this._int8[0] = buff[p + 0];
|
|
1613
|
+
this._int8[1] = buff[p + 1];
|
|
1614
|
+
this._int8[2] = buff[p + 2];
|
|
1615
|
+
this._int8[3] = buff[p + 3];
|
|
1616
|
+
return this._int[0];
|
|
1617
|
+
}
|
|
1618
|
+
writeUintLE(buff, p, n) {
|
|
1619
|
+
this._int[0] = n;
|
|
1620
|
+
buff[p + 0] = this._int8[0];
|
|
1621
|
+
buff[p + 1] = this._int8[1];
|
|
1622
|
+
buff[p + 2] = this._int8[2];
|
|
1623
|
+
buff[p + 3] = this._int8[3];
|
|
1624
|
+
}
|
|
1625
|
+
readASCII(buff, p, l) {
|
|
1626
|
+
let s = "";
|
|
1627
|
+
for (let i = 0; i < l; i++) {
|
|
1628
|
+
s += String.fromCharCode(buff[p + i]);
|
|
1629
|
+
}
|
|
1630
|
+
return s;
|
|
1631
|
+
}
|
|
1632
|
+
writeASCII(buff, p, s) {
|
|
1633
|
+
for (let i = 0; i < s.length; i++) {
|
|
1634
|
+
buff[p + i] = s.charCodeAt(i);
|
|
1635
|
+
}
|
|
1636
|
+
}
|
|
1637
|
+
};
|
|
1638
|
+
|
|
1639
|
+
// src/commands/dds/decode.ts
|
|
1640
|
+
function ddsDecodeCommand() {
|
|
1641
|
+
return new Command13("decode").description("Decode a DDS texture to a 32-bit RGBA PNG").argument("<file>", "path to the .dds file").action(async (file) => {
|
|
1642
|
+
const buffer = await readFile4(file);
|
|
1643
|
+
const frames = new DDSUtils().decode(buffer.buffer);
|
|
1644
|
+
if (frames.length === 0) {
|
|
1645
|
+
console.error("No frames decoded from DDS file");
|
|
1646
|
+
process.exit(1);
|
|
1647
|
+
}
|
|
1648
|
+
const frame = frames[0];
|
|
1649
|
+
const png = new PNG3({ width: frame.width, height: frame.height });
|
|
1650
|
+
png.data = Buffer.from(frame.image);
|
|
1651
|
+
const ext = extname2(file);
|
|
1652
|
+
const outName = basename3(file, ext) + ".png";
|
|
1653
|
+
const outPath = join4(dirname2(file), outName);
|
|
1654
|
+
await writeFile6(outPath, PNG3.sync.write(png));
|
|
1655
|
+
console.log(`Decoded \u2192 ${outPath}`);
|
|
1656
|
+
});
|
|
1657
|
+
}
|
|
1658
|
+
|
|
1659
|
+
// src/commands/dds/encode.ts
|
|
1660
|
+
import { readFile as readFile5, writeFile as writeFile7 } from "fs/promises";
|
|
1661
|
+
import { extname as extname3, join as join5, dirname as dirname3, basename as basename4 } from "path";
|
|
1662
|
+
import { Command as Command14 } from "commander";
|
|
1663
|
+
import { PNG as PNG4 } from "pngjs";
|
|
1664
|
+
function ddsEncodeCommand() {
|
|
1665
|
+
return new Command14("encode").description("Encode a 32-bit RGBA PNG to a DXT5/BC3 DDS texture").argument("<file>", "path to the .png file").action(async (file) => {
|
|
1666
|
+
const buffer = await readFile5(file);
|
|
1667
|
+
const png = PNG4.sync.read(buffer);
|
|
1668
|
+
const ddsBuffer = new DDSUtils().encode(png.data.buffer, png.width, png.height, true);
|
|
1669
|
+
const ext = extname3(file);
|
|
1670
|
+
const outName = basename4(file, ext) + ".dds";
|
|
1671
|
+
const outPath = join5(dirname3(file), outName);
|
|
1672
|
+
await writeFile7(outPath, Buffer.from(ddsBuffer));
|
|
1673
|
+
console.log(`Encoded \u2192 ${outPath}`);
|
|
1674
|
+
});
|
|
1675
|
+
}
|
|
1676
|
+
|
|
1677
|
+
// src/commands/icon/dump-background.ts
|
|
1678
|
+
import { join as join6 } from "path";
|
|
1679
|
+
import { Command as Command15 } from "commander";
|
|
1680
|
+
|
|
1681
|
+
// src/lib/icons/pogs/background-colors.ts
|
|
1682
|
+
var bgColors = {
|
|
1683
|
+
Heavy: [
|
|
1684
|
+
[123, 72, 36],
|
|
1685
|
+
[121, 67, 33],
|
|
1686
|
+
[117, 65, 32],
|
|
1687
|
+
[114, 63, 31],
|
|
1688
|
+
[110, 61, 30],
|
|
1689
|
+
[108, 60, 29],
|
|
1690
|
+
[105, 58, 29],
|
|
1691
|
+
[102, 57, 28],
|
|
1692
|
+
[98, 54, 27],
|
|
1693
|
+
[93, 51, 26],
|
|
1694
|
+
[90, 50, 25],
|
|
1695
|
+
[86, 49, 24],
|
|
1696
|
+
[81, 46, 23],
|
|
1697
|
+
[77, 43, 22],
|
|
1698
|
+
[72, 40, 20],
|
|
1699
|
+
[68, 38, 18],
|
|
1700
|
+
[64, 35, 17],
|
|
1701
|
+
[59, 33, 16],
|
|
1702
|
+
[55, 31, 15],
|
|
1703
|
+
[51, 29, 14],
|
|
1704
|
+
[48, 27, 13],
|
|
1705
|
+
[43, 24, 12],
|
|
1706
|
+
[39, 21, 11],
|
|
1707
|
+
[35, 20, 9]
|
|
1708
|
+
],
|
|
1709
|
+
Light: [
|
|
1710
|
+
[104, 102, 63],
|
|
1711
|
+
[104, 102, 63],
|
|
1712
|
+
[104, 102, 63],
|
|
1713
|
+
[104, 102, 63],
|
|
1714
|
+
[104, 102, 63],
|
|
1715
|
+
[103, 101, 62],
|
|
1716
|
+
[101, 99, 61],
|
|
1717
|
+
[98, 96, 59],
|
|
1718
|
+
[95, 93, 58],
|
|
1719
|
+
[92, 90, 56],
|
|
1720
|
+
[88, 87, 54],
|
|
1721
|
+
[85, 83, 51],
|
|
1722
|
+
[81, 79, 49],
|
|
1723
|
+
[77, 75, 47],
|
|
1724
|
+
[73, 71, 44],
|
|
1725
|
+
[69, 67, 42],
|
|
1726
|
+
[65, 63, 40],
|
|
1727
|
+
[61, 59, 37],
|
|
1728
|
+
[57, 56, 35],
|
|
1729
|
+
[53, 52, 33],
|
|
1730
|
+
[50, 49, 31],
|
|
1731
|
+
[47, 46, 29],
|
|
1732
|
+
[44, 43, 27],
|
|
1733
|
+
[42, 41, 26]
|
|
1734
|
+
],
|
|
1735
|
+
Medium: [
|
|
1736
|
+
[95, 125, 85],
|
|
1737
|
+
[94, 124, 84],
|
|
1738
|
+
[91, 122, 81],
|
|
1739
|
+
[88, 120, 78],
|
|
1740
|
+
[85, 118, 76],
|
|
1741
|
+
[81, 114, 74],
|
|
1742
|
+
[77, 111, 71],
|
|
1743
|
+
[74, 108, 68],
|
|
1744
|
+
[70, 105, 64],
|
|
1745
|
+
[66, 102, 61],
|
|
1746
|
+
[61, 99, 58],
|
|
1747
|
+
[56, 96, 54],
|
|
1748
|
+
[52, 93, 51],
|
|
1749
|
+
[47, 88, 46],
|
|
1750
|
+
[43, 85, 43],
|
|
1751
|
+
[39, 81, 39],
|
|
1752
|
+
[34, 78, 36],
|
|
1753
|
+
[31, 75, 34],
|
|
1754
|
+
[26, 72, 30],
|
|
1755
|
+
[22, 70, 27],
|
|
1756
|
+
[18, 66, 23],
|
|
1757
|
+
[14, 63, 20],
|
|
1758
|
+
[11, 60, 17],
|
|
1759
|
+
[6, 57, 14]
|
|
1760
|
+
],
|
|
1761
|
+
Spg: [
|
|
1762
|
+
[177, 34, 55],
|
|
1763
|
+
[174, 33, 54],
|
|
1764
|
+
[171, 33, 53],
|
|
1765
|
+
[167, 32, 52],
|
|
1766
|
+
[164, 31, 51],
|
|
1767
|
+
[158, 31, 49],
|
|
1768
|
+
[154, 30, 48],
|
|
1769
|
+
[149, 29, 47],
|
|
1770
|
+
[143, 28, 44],
|
|
1771
|
+
[139, 27, 43],
|
|
1772
|
+
[132, 25, 41],
|
|
1773
|
+
[126, 24, 39],
|
|
1774
|
+
[120, 23, 38],
|
|
1775
|
+
[113, 23, 36],
|
|
1776
|
+
[108, 21, 35],
|
|
1777
|
+
[101, 20, 32],
|
|
1778
|
+
[95, 19, 30],
|
|
1779
|
+
[90, 18, 28],
|
|
1780
|
+
[83, 16, 26],
|
|
1781
|
+
[77, 15, 24],
|
|
1782
|
+
[71, 14, 22],
|
|
1783
|
+
[64, 13, 20],
|
|
1784
|
+
[60, 12, 19],
|
|
1785
|
+
[53, 10, 17]
|
|
1786
|
+
],
|
|
1787
|
+
TankDestroyer: [
|
|
1788
|
+
[49, 104, 175],
|
|
1789
|
+
[40, 96, 170],
|
|
1790
|
+
[39, 93, 164],
|
|
1791
|
+
[38, 92, 163],
|
|
1792
|
+
[38, 90, 160],
|
|
1793
|
+
[37, 88, 155],
|
|
1794
|
+
[36, 86, 151],
|
|
1795
|
+
[34, 82, 145],
|
|
1796
|
+
[33, 79, 139],
|
|
1797
|
+
[32, 76, 135],
|
|
1798
|
+
[30, 73, 129],
|
|
1799
|
+
[29, 70, 124],
|
|
1800
|
+
[28, 67, 117],
|
|
1801
|
+
[27, 63, 111],
|
|
1802
|
+
[25, 60, 105],
|
|
1803
|
+
[23, 56, 98],
|
|
1804
|
+
[22, 52, 93],
|
|
1805
|
+
[21, 49, 87],
|
|
1806
|
+
[20, 46, 81],
|
|
1807
|
+
[19, 43, 75],
|
|
1808
|
+
[17, 39, 68],
|
|
1809
|
+
[15, 35, 62],
|
|
1810
|
+
[14, 33, 58],
|
|
1811
|
+
[12, 29, 52]
|
|
1812
|
+
]
|
|
1813
|
+
};
|
|
1814
|
+
|
|
1815
|
+
// src/lib/icons/pogs/pogs-constants.ts
|
|
1816
|
+
var PogsConstants = {
|
|
1817
|
+
width: 80,
|
|
1818
|
+
height: 24
|
|
1819
|
+
};
|
|
1820
|
+
|
|
1821
|
+
// src/lib/icons/ImageBaker.ts
|
|
1822
|
+
import sharp4 from "sharp";
|
|
1823
|
+
var counter = 0;
|
|
1824
|
+
var ImageBaker = class {
|
|
1825
|
+
#box;
|
|
1826
|
+
#layers;
|
|
1827
|
+
#finalizer;
|
|
1828
|
+
constructor(box, layers, finalizer) {
|
|
1829
|
+
this.#box = box;
|
|
1830
|
+
this.#layers = layers;
|
|
1831
|
+
this.#finalizer = finalizer;
|
|
1832
|
+
}
|
|
1833
|
+
async bake(vehicle2) {
|
|
1834
|
+
counter++;
|
|
1835
|
+
logger.debug(`baking ${vehicle2.short_name} (id:${vehicle2.tank_id}) icon (job:${counter})...`);
|
|
1836
|
+
const blank = Buffer.alloc(this.#box.width * this.#box.height * 4, 0);
|
|
1837
|
+
const overlays = [];
|
|
1838
|
+
let prev = null;
|
|
1839
|
+
for (const fn of this.#layers) {
|
|
1840
|
+
let overlay = null;
|
|
1841
|
+
try {
|
|
1842
|
+
overlay = await fn(this.#box, prev, vehicle2);
|
|
1843
|
+
} catch (e) {
|
|
1844
|
+
logger.error("==============");
|
|
1845
|
+
logger.error(`Error processing vehicle ${vehicle2.short_name} (${vehicle2.tank_id})`);
|
|
1846
|
+
logger.error(e);
|
|
1847
|
+
logger.error("==============");
|
|
1848
|
+
process.exit(1);
|
|
1849
|
+
}
|
|
1850
|
+
if (overlay) {
|
|
1851
|
+
overlays.push(overlay);
|
|
1852
|
+
}
|
|
1853
|
+
prev = overlay;
|
|
1854
|
+
}
|
|
1855
|
+
const result = sharp4(blank, { raw: { width: this.#box.width, height: this.#box.height, channels: 4 } }).composite(overlays);
|
|
1856
|
+
const final = this.#finalizer ? this.#finalizer(result) : result;
|
|
1857
|
+
logger.debug(`. baked ${vehicle2.short_name} (${vehicle2.tank_id}) icon successfully
|
|
1858
|
+
`);
|
|
1859
|
+
return final;
|
|
1860
|
+
}
|
|
1861
|
+
};
|
|
1862
|
+
|
|
1863
|
+
// src/lib/icons/layers/gradient-background.ts
|
|
1864
|
+
var TYPE_TO_KEY = {
|
|
1865
|
+
lightTank: "Light",
|
|
1866
|
+
mediumTank: "Medium",
|
|
1867
|
+
heavyTank: "Heavy",
|
|
1868
|
+
"AT-SPG": "TankDestroyer",
|
|
1869
|
+
SPG: "Spg"
|
|
1870
|
+
};
|
|
1871
|
+
function gradientBackground(colors = bgColors) {
|
|
1872
|
+
const cache2 = /* @__PURE__ */ new Map();
|
|
1873
|
+
return async (box, _prev, vehicle2) => {
|
|
1874
|
+
const key = TYPE_TO_KEY[vehicle2.type];
|
|
1875
|
+
const rows = colors[key];
|
|
1876
|
+
if (!rows) {
|
|
1877
|
+
throw new Error(`No gradient colors for vehicle type: ${vehicle2.type}`);
|
|
1878
|
+
}
|
|
1879
|
+
let overlay = cache2.get(key);
|
|
1880
|
+
if (overlay === void 0) {
|
|
1881
|
+
const height = rows.length;
|
|
1882
|
+
const pixels = Buffer.alloc(box.width * height * 4);
|
|
1883
|
+
for (let y = 0; y < height; y++) {
|
|
1884
|
+
const [r, g, b] = rows[y];
|
|
1885
|
+
for (let x = 0; x < box.width; x++) {
|
|
1886
|
+
const i = (y * box.width + x) * 4;
|
|
1887
|
+
pixels[i] = r;
|
|
1888
|
+
pixels[i + 1] = g;
|
|
1889
|
+
pixels[i + 2] = b;
|
|
1890
|
+
pixels[i + 3] = 255;
|
|
1891
|
+
}
|
|
1892
|
+
}
|
|
1893
|
+
overlay = {
|
|
1894
|
+
input: pixels,
|
|
1895
|
+
raw: { width: box.width, height, channels: 4 },
|
|
1896
|
+
left: 0,
|
|
1897
|
+
top: 0,
|
|
1898
|
+
meta: { width: box.width, height, left: 0, top: 0 }
|
|
1899
|
+
};
|
|
1900
|
+
cache2.set(key, overlay);
|
|
1901
|
+
}
|
|
1902
|
+
return overlay;
|
|
1903
|
+
};
|
|
1904
|
+
}
|
|
1905
|
+
|
|
1906
|
+
// src/lib/icons/layers/bar-and-shield.ts
|
|
1907
|
+
import sharp5 from "sharp";
|
|
1908
|
+
var SHIELD_PATH = resolveAsset(new URL(import.meta.url), "pogs/sheild.png");
|
|
1909
|
+
var STRIPE_PATH = resolveAsset(new URL(import.meta.url), "pogs/stripe.png");
|
|
1910
|
+
function barAndShield() {
|
|
1911
|
+
const cache2 = /* @__PURE__ */ new Map();
|
|
1912
|
+
return async (box, _prev, _vehicle) => {
|
|
1913
|
+
const key = `${box.width}x${box.height}`;
|
|
1914
|
+
let overlay = cache2.get(key);
|
|
1915
|
+
if (overlay === void 0) {
|
|
1916
|
+
const blank = Buffer.alloc(box.width * box.height * 4, 0);
|
|
1917
|
+
const { data } = await sharp5(blank, { raw: { width: box.width, height: box.height, channels: 4 } }).composite([
|
|
1918
|
+
{ input: STRIPE_PATH, top: 0, left: 0 },
|
|
1919
|
+
{ input: SHIELD_PATH, top: 2, left: 1 }
|
|
1920
|
+
]).raw().toBuffer({ resolveWithObject: true });
|
|
1921
|
+
overlay = { input: data, raw: { width: box.width, height: box.height, channels: 4 }, left: 0, top: 0, meta: { width: box.width, height: box.height, left: 0, top: 0 } };
|
|
1922
|
+
cache2.set(key, overlay);
|
|
1923
|
+
}
|
|
1924
|
+
return overlay;
|
|
1925
|
+
};
|
|
1926
|
+
}
|
|
1927
|
+
|
|
1928
|
+
// src/commands/icon/dump-background.ts
|
|
1929
|
+
function dumpBackgroundCommand() {
|
|
1930
|
+
return new Command15("dump-background").description("Write a composited tank-type icon background (gradient + shield) for each type to cwd").action(async () => {
|
|
1931
|
+
const baker = new ImageBaker(PogsConstants, [
|
|
1932
|
+
gradientBackground(),
|
|
1933
|
+
barAndShield()
|
|
1934
|
+
]);
|
|
1935
|
+
for (const type of Object.keys(bgColors)) {
|
|
1936
|
+
const stub = { type };
|
|
1937
|
+
const outPath = join6(process.cwd(), `${type.toLowerCase()}.png`);
|
|
1938
|
+
await (await baker.bake(stub)).png().toFile(outPath);
|
|
1939
|
+
console.log(`Written ${outPath}`);
|
|
1940
|
+
}
|
|
1941
|
+
});
|
|
1942
|
+
}
|
|
1943
|
+
|
|
1944
|
+
// src/commands/icon/render.ts
|
|
1945
|
+
import { existsSync as existsSync2, mkdirSync as mkdirSync2 } from "fs";
|
|
1946
|
+
import { join as join7 } from "path";
|
|
1947
|
+
import { Command as Command16 } from "commander";
|
|
1948
|
+
|
|
1949
|
+
// src/lib/icons/layers/vehicle-icon.ts
|
|
1950
|
+
import sharp6 from "sharp";
|
|
1951
|
+
|
|
1952
|
+
// src/lib/utils.ts
|
|
1953
|
+
function darkenIcon(buffer, factor) {
|
|
1954
|
+
const out = Buffer.from(buffer);
|
|
1955
|
+
for (let i = 0; i < out.length; i += 4) {
|
|
1956
|
+
if (out[i] === 0 && out[i + 1] === 0 && out[i + 2] === 0) {
|
|
1957
|
+
continue;
|
|
1958
|
+
}
|
|
1959
|
+
out[i] = Math.round(out[i] * factor);
|
|
1960
|
+
out[i + 1] = Math.round(out[i + 1] * factor);
|
|
1961
|
+
out[i + 2] = Math.round(out[i + 2] * factor);
|
|
1962
|
+
}
|
|
1963
|
+
return out;
|
|
1964
|
+
}
|
|
1965
|
+
|
|
1966
|
+
// src/lib/box-utils/Aligner.ts
|
|
1967
|
+
var ALIASES = {
|
|
1968
|
+
// top-left
|
|
1969
|
+
tl: "tl",
|
|
1970
|
+
lt: "tl",
|
|
1971
|
+
"top-left": "tl",
|
|
1972
|
+
"left-top": "tl",
|
|
1973
|
+
// top-middle
|
|
1974
|
+
tm: "tm",
|
|
1975
|
+
tc: "tm",
|
|
1976
|
+
ct: "tm",
|
|
1977
|
+
top: "tm",
|
|
1978
|
+
"top-center": "tm",
|
|
1979
|
+
"top-middle": "tm",
|
|
1980
|
+
"center-top": "tm",
|
|
1981
|
+
"middle-top": "tm",
|
|
1982
|
+
// top-right
|
|
1983
|
+
tr: "tr",
|
|
1984
|
+
rt: "tr",
|
|
1985
|
+
"top-right": "tr",
|
|
1986
|
+
"right-top": "tr",
|
|
1987
|
+
// left-middle
|
|
1988
|
+
lm: "lm",
|
|
1989
|
+
lc: "lm",
|
|
1990
|
+
cl: "lm",
|
|
1991
|
+
ml: "lm",
|
|
1992
|
+
left: "lm",
|
|
1993
|
+
"left-center": "lm",
|
|
1994
|
+
"left-middle": "lm",
|
|
1995
|
+
"center-left": "lm",
|
|
1996
|
+
"middle-left": "lm",
|
|
1997
|
+
// center
|
|
1998
|
+
c: "c",
|
|
1999
|
+
m: "c",
|
|
2000
|
+
cc: "c",
|
|
2001
|
+
center: "c",
|
|
2002
|
+
middle: "c",
|
|
2003
|
+
// right-middle
|
|
2004
|
+
rm: "rm",
|
|
2005
|
+
rc: "rm",
|
|
2006
|
+
cr: "rm",
|
|
2007
|
+
mr: "rm",
|
|
2008
|
+
right: "rm",
|
|
2009
|
+
"right-center": "rm",
|
|
2010
|
+
"right-middle": "rm",
|
|
2011
|
+
"center-right": "rm",
|
|
2012
|
+
"middle-right": "rm",
|
|
2013
|
+
// bottom-left
|
|
2014
|
+
bl: "bl",
|
|
2015
|
+
lb: "bl",
|
|
2016
|
+
"bottom-left": "bl",
|
|
2017
|
+
"left-bottom": "bl",
|
|
2018
|
+
// bottom-middle
|
|
2019
|
+
bm: "bm",
|
|
2020
|
+
bc: "bm",
|
|
2021
|
+
cb: "bm",
|
|
2022
|
+
mb: "bm",
|
|
2023
|
+
bottom: "bm",
|
|
2024
|
+
"bottom-center": "bm",
|
|
2025
|
+
"bottom-middle": "bm",
|
|
2026
|
+
"center-bottom": "bm",
|
|
2027
|
+
"middle-bottom": "bm",
|
|
2028
|
+
// bottom-right
|
|
2029
|
+
br: "br",
|
|
2030
|
+
rb: "br",
|
|
2031
|
+
"bottom-right": "br",
|
|
2032
|
+
"right-bottom": "br"
|
|
2033
|
+
};
|
|
2034
|
+
var OFFSETS = {
|
|
2035
|
+
tl: [0, 0],
|
|
2036
|
+
tm: [0.5, 0],
|
|
2037
|
+
tr: [1, 0],
|
|
2038
|
+
lm: [0, 0.5],
|
|
2039
|
+
c: [0.5, 0.5],
|
|
2040
|
+
rm: [1, 0.5],
|
|
2041
|
+
bl: [0, 1],
|
|
2042
|
+
bm: [0.5, 1],
|
|
2043
|
+
br: [1, 1]
|
|
2044
|
+
};
|
|
2045
|
+
var ROUND_SUFFIXES = /* @__PURE__ */ new Set([".u", ".d", ".+", ".-"]);
|
|
2046
|
+
var INNER_RE = /^(t|l|c|r|b|w|bw|h|bh)(?:\s*([+\-\/])\s*(\d+(?:\.\d+)?))?$/;
|
|
2047
|
+
var Aligner = class _Aligner {
|
|
2048
|
+
params;
|
|
2049
|
+
#bx;
|
|
2050
|
+
#by;
|
|
2051
|
+
#sx = 0;
|
|
2052
|
+
#sy = 0;
|
|
2053
|
+
constructor(box, rectAnchor, boxAnchor) {
|
|
2054
|
+
this.params = { box, rectAnchor, boxAnchor };
|
|
2055
|
+
[this.#bx, this.#by] = Array.isArray(boxAnchor) ? [_Aligner.#resolveAxisExpr(boxAnchor[0], box.width, box.height, "x"), _Aligner.#resolveAxisExpr(boxAnchor[1], box.width, box.height, "y")] : _Aligner.#resolveAnchorWithRound(boxAnchor, box.width, box.height);
|
|
2056
|
+
}
|
|
2057
|
+
align(rect) {
|
|
2058
|
+
const [ox, oy] = _Aligner.#resolveAnchorWithRound(this.params.rectAnchor, rect.width, rect.height);
|
|
2059
|
+
return { left: this.#bx + this.#sx - ox, top: this.#by + this.#sy - oy, width: rect.width, height: rect.height };
|
|
2060
|
+
}
|
|
2061
|
+
clone(overrides) {
|
|
2062
|
+
const a = new _Aligner(
|
|
2063
|
+
overrides?.box ?? this.params.box,
|
|
2064
|
+
overrides?.rectAnchor ?? this.params.rectAnchor,
|
|
2065
|
+
overrides?.boxAnchor ?? this.params.boxAnchor
|
|
2066
|
+
);
|
|
2067
|
+
a.#sx = this.#sx;
|
|
2068
|
+
a.#sy = this.#sy;
|
|
2069
|
+
return a;
|
|
2070
|
+
}
|
|
2071
|
+
shift(x, y) {
|
|
2072
|
+
const a = new _Aligner(this.params.box, this.params.rectAnchor, this.params.boxAnchor);
|
|
2073
|
+
a.#sx = this.#sx + x;
|
|
2074
|
+
a.#sy = this.#sy + y;
|
|
2075
|
+
return a;
|
|
2076
|
+
}
|
|
2077
|
+
static #applySuffixRound(suffix, n) {
|
|
2078
|
+
return suffix === ".u" || suffix === ".+" ? Math.ceil(n) : Math.floor(n);
|
|
2079
|
+
}
|
|
2080
|
+
static #resolveOffset(alias, w, h) {
|
|
2081
|
+
const [fx, fy] = OFFSETS[ALIASES[alias]];
|
|
2082
|
+
return [fx * w, fy * h];
|
|
2083
|
+
}
|
|
2084
|
+
static #resolveAnchorWithRound(alias, w, h) {
|
|
2085
|
+
const last2 = alias.slice(-2);
|
|
2086
|
+
const hasSuffix = ROUND_SUFFIXES.has(last2);
|
|
2087
|
+
const pureAlias = hasSuffix ? alias.slice(0, -2) : alias;
|
|
2088
|
+
const suffix = hasSuffix ? last2 : void 0;
|
|
2089
|
+
const [rx, ry] = _Aligner.#resolveOffset(pureAlias, w, h);
|
|
2090
|
+
return [_Aligner.#applySuffixRound(suffix, rx), _Aligner.#applySuffixRound(suffix, ry)];
|
|
2091
|
+
}
|
|
2092
|
+
static #resolveDimRef(dim, bw, bh, axis) {
|
|
2093
|
+
if (dim === "t" || dim === "l") {
|
|
2094
|
+
return 0;
|
|
2095
|
+
}
|
|
2096
|
+
if (dim === "c") {
|
|
2097
|
+
return axis === "x" ? bw / 2 : bh / 2;
|
|
2098
|
+
}
|
|
2099
|
+
if (dim === "r" || dim === "w" || dim === "bw") {
|
|
2100
|
+
return bw;
|
|
2101
|
+
}
|
|
2102
|
+
return bh;
|
|
2103
|
+
}
|
|
2104
|
+
static #resolveInner(expr, bw, bh, axis) {
|
|
2105
|
+
const match = INNER_RE.exec(expr.trim());
|
|
2106
|
+
if (!match) {
|
|
2107
|
+
throw new Error(`Invalid axis expression: "${expr}"`);
|
|
2108
|
+
}
|
|
2109
|
+
const [, dim, op, numStr] = match;
|
|
2110
|
+
const base = _Aligner.#resolveDimRef(dim, bw, bh, axis);
|
|
2111
|
+
if (!op) {
|
|
2112
|
+
return base;
|
|
2113
|
+
}
|
|
2114
|
+
const n = Number(numStr);
|
|
2115
|
+
if (op === "+") {
|
|
2116
|
+
return base + n;
|
|
2117
|
+
}
|
|
2118
|
+
if (op === "-") {
|
|
2119
|
+
return base - n;
|
|
2120
|
+
}
|
|
2121
|
+
return base / n;
|
|
2122
|
+
}
|
|
2123
|
+
static #resolveAxisExpr(expr, bw, bh, axis) {
|
|
2124
|
+
if (typeof expr === "number") {
|
|
2125
|
+
return expr;
|
|
2126
|
+
}
|
|
2127
|
+
const s = String(expr);
|
|
2128
|
+
const groupMatch = /^\((.+)\)([.][ud+\-])$/.exec(s);
|
|
2129
|
+
if (groupMatch) {
|
|
2130
|
+
return _Aligner.#applySuffixRound(groupMatch[2], _Aligner.#resolveInner(groupMatch[1], bw, bh, axis));
|
|
2131
|
+
}
|
|
2132
|
+
const last2 = s.slice(-2);
|
|
2133
|
+
if (ROUND_SUFFIXES.has(last2)) {
|
|
2134
|
+
return _Aligner.#applySuffixRound(last2, _Aligner.#resolveInner(s.slice(0, -2).trim(), bw, bh, axis));
|
|
2135
|
+
}
|
|
2136
|
+
return Math.floor(_Aligner.#resolveInner(s, bw, bh, axis));
|
|
2137
|
+
}
|
|
2138
|
+
};
|
|
2139
|
+
|
|
2140
|
+
// src/lib/box-utils/index.ts
|
|
2141
|
+
function createAligner(box, rectAnchor, boxAnchor) {
|
|
2142
|
+
return new Aligner(box, rectAnchor, boxAnchor);
|
|
2143
|
+
}
|
|
2144
|
+
|
|
2145
|
+
// src/lib/icons/layers/vehicle-icon.ts
|
|
2146
|
+
var MAX_ICON_W = 62;
|
|
2147
|
+
var MAX_ICON_H = 15;
|
|
2148
|
+
var defaultAligner = createAligner(PogsConstants, "bm.+", [40, "bh"]);
|
|
2149
|
+
function fitToBox(srcW, srcH) {
|
|
2150
|
+
const aspect = srcW / srcH;
|
|
2151
|
+
let w = srcW;
|
|
2152
|
+
let h = srcH;
|
|
2153
|
+
if (w > MAX_ICON_W) {
|
|
2154
|
+
w = MAX_ICON_W;
|
|
2155
|
+
h = Math.trunc(w / aspect);
|
|
2156
|
+
}
|
|
2157
|
+
if (h > MAX_ICON_H) {
|
|
2158
|
+
h = MAX_ICON_H;
|
|
2159
|
+
w = Math.trunc(h * aspect);
|
|
2160
|
+
}
|
|
2161
|
+
return { width: w, height: h };
|
|
2162
|
+
}
|
|
2163
|
+
function vehicleIcon(data, aligner = defaultAligner) {
|
|
2164
|
+
return async (box, _prev, vehicle2) => {
|
|
2165
|
+
const iconResult = await data.getDefaultVehicleIcon(vehicle2, "medium");
|
|
2166
|
+
if (!iconResult) {
|
|
2167
|
+
return null;
|
|
2168
|
+
}
|
|
2169
|
+
const { data: iconData, info: iconInfo } = iconResult;
|
|
2170
|
+
const { width: iconW, height: iconH } = fitToBox(iconInfo.width, iconInfo.height);
|
|
2171
|
+
const { data: scaledData, info: scaledInfo } = await sharp6(darkenIcon(iconData, 0.5), {
|
|
2172
|
+
raw: { width: iconInfo.width, height: iconInfo.height, channels: iconInfo.channels }
|
|
2173
|
+
}).resize(iconW, iconH).raw().toBuffer({ resolveWithObject: true });
|
|
2174
|
+
const { data: trimmedData, info: trimmedInfo } = await sharp6(scaledData, {
|
|
2175
|
+
raw: { width: scaledInfo.width, height: scaledInfo.height, channels: scaledInfo.channels }
|
|
2176
|
+
}).trim({ threshold: 13 }).raw().toBuffer({ resolveWithObject: true });
|
|
2177
|
+
const { width, height } = trimmedInfo;
|
|
2178
|
+
const { left, top } = aligner.align({ width: trimmedInfo.width, height: trimmedInfo.height });
|
|
2179
|
+
return {
|
|
2180
|
+
input: trimmedData,
|
|
2181
|
+
raw: { width, height, channels: trimmedInfo.channels },
|
|
2182
|
+
left,
|
|
2183
|
+
top,
|
|
2184
|
+
meta: { width, height, left, top }
|
|
2185
|
+
};
|
|
2186
|
+
};
|
|
2187
|
+
}
|
|
2188
|
+
|
|
2189
|
+
// src/lib/icons/layers/tier-text.ts
|
|
2190
|
+
var defaultAligner2 = createAligner(PogsConstants, "tm.+", [10, 5]);
|
|
2191
|
+
function tierText(aligner = defaultAligner2) {
|
|
2192
|
+
const cache2 = /* @__PURE__ */ new Map();
|
|
2193
|
+
return async (_box, _prev, vehicle2) => {
|
|
2194
|
+
let rendered = cache2.get(vehicle2.tier);
|
|
2195
|
+
if (rendered === void 0) {
|
|
2196
|
+
const fontName = vehicle2.tier > 12 ? "pogsNumbersBold" : "pogsNumbers";
|
|
2197
|
+
rendered = await renderWithShadow(fontName, String(vehicle2.tier));
|
|
2198
|
+
cache2.set(vehicle2.tier, rendered);
|
|
2199
|
+
}
|
|
2200
|
+
const { width, height } = rendered;
|
|
2201
|
+
const text = String(vehicle2.tier);
|
|
2202
|
+
const { left, top } = aligner.align(rendered);
|
|
2203
|
+
return {
|
|
2204
|
+
input: rendered.data,
|
|
2205
|
+
raw: { width, height, channels: 4 },
|
|
2206
|
+
left,
|
|
2207
|
+
top,
|
|
2208
|
+
meta: { width, height, left, top, text }
|
|
2209
|
+
};
|
|
2210
|
+
};
|
|
2211
|
+
}
|
|
2212
|
+
|
|
2213
|
+
// src/lib/icons/layers/name-text.ts
|
|
2214
|
+
var NAME_X = 18;
|
|
2215
|
+
var NAME_Y = 2;
|
|
2216
|
+
function nameText() {
|
|
2217
|
+
return async (_box, _prev, vehicle2) => {
|
|
2218
|
+
const alias = lookupShortName(vehicle2).toLowerCase();
|
|
2219
|
+
const { data, width, height } = await renderWithShadow("pogs4px", alias);
|
|
2220
|
+
return { input: data, raw: { width, height, channels: 4 }, left: NAME_X, top: NAME_Y, meta: { width, height, left: NAME_X, top: NAME_Y, text: alias } };
|
|
2221
|
+
};
|
|
2222
|
+
}
|
|
2223
|
+
|
|
2224
|
+
// src/lib/icons/pogs/PogsClear.ts
|
|
2225
|
+
var iconAligner = createAligner(PogsConstants, "bl.+", [18, 1]);
|
|
2226
|
+
var PogsClear = class {
|
|
2227
|
+
createBaker(app2) {
|
|
2228
|
+
return new ImageBaker(
|
|
2229
|
+
PogsConstants,
|
|
2230
|
+
[barAndShield(), vehicleIcon(app2, iconAligner), tierText(), nameText()]
|
|
2231
|
+
);
|
|
2232
|
+
}
|
|
2233
|
+
};
|
|
2234
|
+
|
|
2235
|
+
// src/lib/icons/pogs/PogsColor.ts
|
|
2236
|
+
var iconAligner2 = createAligner(PogsConstants, "bl.+", [18, 1]);
|
|
2237
|
+
var PogsColor = class {
|
|
2238
|
+
createBaker(app2) {
|
|
2239
|
+
return new ImageBaker(
|
|
2240
|
+
PogsConstants,
|
|
2241
|
+
[
|
|
2242
|
+
gradientBackground(),
|
|
2243
|
+
barAndShield(),
|
|
2244
|
+
vehicleIcon(app2, iconAligner2),
|
|
2245
|
+
tierText(),
|
|
2246
|
+
nameText()
|
|
2247
|
+
],
|
|
2248
|
+
(s) => s.removeAlpha()
|
|
2249
|
+
);
|
|
2250
|
+
}
|
|
2251
|
+
};
|
|
2252
|
+
|
|
2253
|
+
// src/lib/icons/layers/pre-rendered-background.ts
|
|
2254
|
+
import { readFile as readFile6 } from "fs/promises";
|
|
2255
|
+
import { resolve as resolve4 } from "path";
|
|
2256
|
+
import sharp7 from "sharp";
|
|
2257
|
+
var TYPE_MAP = {
|
|
2258
|
+
lightTank: "lt",
|
|
2259
|
+
mediumTank: "mt",
|
|
2260
|
+
heavyTank: "ht",
|
|
2261
|
+
"AT-SPG": "td",
|
|
2262
|
+
SPG: "at"
|
|
2263
|
+
};
|
|
2264
|
+
var selfUrl = new URL(import.meta.url);
|
|
2265
|
+
function preRenderedBackground(version2, flavor = "") {
|
|
2266
|
+
const cache2 = /* @__PURE__ */ new Map();
|
|
2267
|
+
const dir = resolveAsset(selfUrl, `pogs-fixed/pre-rendered/combined-v${version2}`);
|
|
2268
|
+
return async (box, _prev, vehicle2) => {
|
|
2269
|
+
if (version2 === 1) {
|
|
2270
|
+
flavor = "";
|
|
2271
|
+
}
|
|
2272
|
+
const isPrem = vehicle2.is_premium || vehicle2.is_premium_igr || vehicle2.is_gift;
|
|
2273
|
+
const mappedType = TYPE_MAP[vehicle2.type];
|
|
2274
|
+
const flavorSuffix = flavor ? `.${flavor}` : "";
|
|
2275
|
+
const premSuffix = isPrem ? ".prem" : "";
|
|
2276
|
+
const filename = `${mappedType}${flavorSuffix}${premSuffix}.png`;
|
|
2277
|
+
let overlay = cache2.get(filename);
|
|
2278
|
+
if (overlay === void 0) {
|
|
2279
|
+
const filePath = resolve4(dir, filename);
|
|
2280
|
+
const fileBuffer = await readFile6(filePath);
|
|
2281
|
+
const { data } = await sharp7(fileBuffer).ensureAlpha().extract({ left: 0, top: 0, width: box.width, height: box.height }).raw().toBuffer({ resolveWithObject: true });
|
|
2282
|
+
overlay = { input: data, raw: { width: box.width, height: box.height, channels: 4 }, left: 0, top: 0, meta: { width: box.width, height: box.height, left: 0, top: 0 } };
|
|
2283
|
+
cache2.set(filename, overlay);
|
|
2284
|
+
}
|
|
2285
|
+
return overlay;
|
|
2286
|
+
};
|
|
2287
|
+
}
|
|
2288
|
+
|
|
2289
|
+
// src/lib/icons/pogs/PogsClearV1.ts
|
|
2290
|
+
var iconAligner3 = createAligner(PogsConstants, "bl.+", [18, "(bh - 1).+"]);
|
|
2291
|
+
var PogsClearV1 = class {
|
|
2292
|
+
version = 1;
|
|
2293
|
+
createBaker(app2) {
|
|
2294
|
+
return new ImageBaker(
|
|
2295
|
+
PogsConstants,
|
|
2296
|
+
[
|
|
2297
|
+
preRenderedBackground(this.version, "clear"),
|
|
2298
|
+
vehicleIcon(app2, iconAligner3),
|
|
2299
|
+
tierText(),
|
|
2300
|
+
nameText()
|
|
2301
|
+
]
|
|
2302
|
+
);
|
|
2303
|
+
}
|
|
2304
|
+
};
|
|
2305
|
+
|
|
2306
|
+
// src/lib/icons/pogs/PogsClearV2.ts
|
|
2307
|
+
var PogsClearV2 = class extends PogsClearV1 {
|
|
2308
|
+
version = 2;
|
|
2309
|
+
};
|
|
2310
|
+
|
|
2311
|
+
// src/lib/icons/layers/text-view-range.ts
|
|
2312
|
+
var defaultAligner3 = createAligner(PogsConstants, "br.+", [37, "bh - 1"]);
|
|
2313
|
+
function textViewRange(app2, aligner = defaultAligner3) {
|
|
2314
|
+
return async (_box, _prev, vehicle2) => {
|
|
2315
|
+
const profiles = await app2.getStatsForBestConfig(vehicle2);
|
|
2316
|
+
const viewRange = profiles[vehicle2.tank_id]?.turret?.view_range;
|
|
2317
|
+
if (viewRange === void 0) {
|
|
2318
|
+
return null;
|
|
2319
|
+
}
|
|
2320
|
+
const { data, width, height } = await renderWithShadow("pogs4px", viewRange, Colors.beige);
|
|
2321
|
+
const { left, top } = aligner.align({ width, height });
|
|
2322
|
+
return {
|
|
2323
|
+
input: data,
|
|
2324
|
+
raw: { width, height, channels: 4 },
|
|
2325
|
+
left,
|
|
2326
|
+
top,
|
|
2327
|
+
meta: { width, height, left, top, text: viewRange }
|
|
2328
|
+
};
|
|
2329
|
+
};
|
|
2330
|
+
}
|
|
2331
|
+
|
|
2332
|
+
// src/lib/icons/layers/text-reload.ts
|
|
2333
|
+
var defaultAligner4 = createAligner(PogsConstants, "br.+", [37, "bh - 8"]);
|
|
2334
|
+
function textReload(app2, aligner = defaultAligner4) {
|
|
2335
|
+
return async (_box, _prev, vehicle2) => {
|
|
2336
|
+
const profiles = await app2.getStatsForBestConfig(vehicle2);
|
|
2337
|
+
const reloadTime = profiles[vehicle2.tank_id]?.gun?.reload_time;
|
|
2338
|
+
if (reloadTime === void 0) {
|
|
2339
|
+
return null;
|
|
2340
|
+
}
|
|
2341
|
+
const { data, width, height } = await renderWithShadow("pogs4px", reloadTime, Colors.yellow);
|
|
2342
|
+
const { left, top } = aligner.align({ width, height });
|
|
2343
|
+
return {
|
|
2344
|
+
input: data,
|
|
2345
|
+
raw: { width, height, channels: 4 },
|
|
2346
|
+
left,
|
|
2347
|
+
top,
|
|
2348
|
+
meta: { width, height, left, top, text: reloadTime }
|
|
2349
|
+
};
|
|
2350
|
+
};
|
|
2351
|
+
}
|
|
2352
|
+
|
|
2353
|
+
// src/lib/icons/layers/text-hull-armor.ts
|
|
2354
|
+
var defaultAligner5 = createAligner(PogsConstants, "br", ["r", "b - 1"]);
|
|
2355
|
+
function textHullArmor(app2, aligner = defaultAligner5) {
|
|
2356
|
+
return async (_box, _prev, vehicle2) => {
|
|
2357
|
+
const profiles = await app2.getStatsForBestConfig(vehicle2);
|
|
2358
|
+
const hull = profiles[vehicle2.tank_id]?.armor?.hull;
|
|
2359
|
+
if (hull === void 0) {
|
|
2360
|
+
return null;
|
|
2361
|
+
}
|
|
2362
|
+
const text = `${hull.front}*${hull.sides}*${hull.rear}`;
|
|
2363
|
+
const { data, width, height } = await renderWithShadow("pogs4px", text, Colors.white);
|
|
2364
|
+
const { left, top } = aligner.align({ width, height });
|
|
2365
|
+
return {
|
|
2366
|
+
input: data,
|
|
2367
|
+
raw: { width, height, channels: 4 },
|
|
2368
|
+
left,
|
|
2369
|
+
top,
|
|
2370
|
+
meta: { width, height, left, top, text }
|
|
2371
|
+
};
|
|
2372
|
+
};
|
|
2373
|
+
}
|
|
2374
|
+
|
|
2375
|
+
// src/lib/icons/layers/text-turret-armor.ts
|
|
2376
|
+
var defaultAligner6 = createAligner(PogsConstants, "br", ["r", "b - 8"]);
|
|
2377
|
+
function textTurretArmor(app2, aligner = defaultAligner6) {
|
|
2378
|
+
return async (_box, prev, vehicle2) => {
|
|
2379
|
+
const profiles = await app2.getStatsForBestConfig(vehicle2);
|
|
2380
|
+
const turret = profiles[vehicle2.tank_id]?.armor?.turret;
|
|
2381
|
+
if (!turret) {
|
|
2382
|
+
return null;
|
|
2383
|
+
}
|
|
2384
|
+
const text = `${turret.front}*`;
|
|
2385
|
+
const { data, width, height } = await renderWithShadow("pogs4px", text, Colors.white);
|
|
2386
|
+
const offsetX = -1 * prev.meta.width + width;
|
|
2387
|
+
const shifted = aligner.shift(offsetX, 0);
|
|
2388
|
+
const { left, top } = shifted.align({ width, height });
|
|
2389
|
+
return {
|
|
2390
|
+
input: data,
|
|
2391
|
+
raw: { width, height, channels: 4 },
|
|
2392
|
+
left,
|
|
2393
|
+
top,
|
|
2394
|
+
meta: { width, height, left, top, text }
|
|
2395
|
+
};
|
|
2396
|
+
};
|
|
2397
|
+
}
|
|
2398
|
+
|
|
2399
|
+
// src/lib/icons/layers/text-penetration.ts
|
|
2400
|
+
var defaultAligner7 = createAligner(PogsConstants, "rt", ["r", 2]);
|
|
2401
|
+
function textPenetration(app2, aligner = defaultAligner7) {
|
|
2402
|
+
return async (_box, _prev, vehicle2) => {
|
|
2403
|
+
const profiles = await app2.getStatsForBestConfig(vehicle2);
|
|
2404
|
+
const text = profiles[vehicle2.tank_id]?.ammo?.[0].penetration?.[1];
|
|
2405
|
+
if (text === void 0) {
|
|
2406
|
+
return null;
|
|
2407
|
+
}
|
|
2408
|
+
const { data, width, height } = await renderWithShadow("pogs4px", text, Colors.beige);
|
|
2409
|
+
const { left, top } = aligner.align({ width, height });
|
|
2410
|
+
return {
|
|
2411
|
+
input: data,
|
|
2412
|
+
raw: { width, height, channels: 4 },
|
|
2413
|
+
left,
|
|
2414
|
+
top,
|
|
2415
|
+
meta: { width, height, left, top, text }
|
|
2416
|
+
};
|
|
2417
|
+
};
|
|
2418
|
+
}
|
|
2419
|
+
|
|
2420
|
+
// src/lib/icons/layers/text-damage.ts
|
|
2421
|
+
var defaultAligner8 = createAligner(PogsConstants, "rt", ["r", 9]);
|
|
2422
|
+
function textDamage(app2, aligner = defaultAligner8) {
|
|
2423
|
+
return async (_box, _prev, vehicle2) => {
|
|
2424
|
+
const profiles = await app2.getStatsForBestConfig(vehicle2);
|
|
2425
|
+
const text = profiles[vehicle2.tank_id]?.ammo?.[0].damage?.[1];
|
|
2426
|
+
if (text === void 0) {
|
|
2427
|
+
return null;
|
|
2428
|
+
}
|
|
2429
|
+
const { data, width, height } = await renderWithShadow("pogs4px", text, Colors.beige);
|
|
2430
|
+
const { left, top } = aligner.align({ width, height });
|
|
2431
|
+
return {
|
|
2432
|
+
input: data,
|
|
2433
|
+
raw: { width, height, channels: 4 },
|
|
2434
|
+
left,
|
|
2435
|
+
top,
|
|
2436
|
+
meta: { width, height, left, top, text }
|
|
2437
|
+
};
|
|
2438
|
+
};
|
|
2439
|
+
}
|
|
2440
|
+
|
|
2441
|
+
// src/lib/icons/pogs/PogsColorV1.ts
|
|
2442
|
+
var PogsColorV1 = class {
|
|
2443
|
+
version = 1;
|
|
2444
|
+
createBaker(data) {
|
|
2445
|
+
return new ImageBaker(
|
|
2446
|
+
PogsConstants,
|
|
2447
|
+
[
|
|
2448
|
+
preRenderedBackground(this.version, ""),
|
|
2449
|
+
vehicleIcon(data),
|
|
2450
|
+
textViewRange(data),
|
|
2451
|
+
textReload(data),
|
|
2452
|
+
textPenetration(data),
|
|
2453
|
+
textDamage(data),
|
|
2454
|
+
textHullArmor(data),
|
|
2455
|
+
textTurretArmor(data),
|
|
2456
|
+
tierText(),
|
|
2457
|
+
nameText()
|
|
2458
|
+
],
|
|
2459
|
+
(s) => s.removeAlpha()
|
|
2460
|
+
);
|
|
2461
|
+
}
|
|
2462
|
+
};
|
|
2463
|
+
|
|
2464
|
+
// src/lib/icons/pogs/PogsColorV2.ts
|
|
2465
|
+
var PogsColorV2 = class extends PogsColorV1 {
|
|
2466
|
+
version = 2;
|
|
2467
|
+
};
|
|
2468
|
+
|
|
2469
|
+
// src/commands/icon/render.ts
|
|
2470
|
+
var CONCURRENCY = 5;
|
|
2471
|
+
async function renderOneToFile(vehicle2, baker, outDir) {
|
|
2472
|
+
const outPath = join7(outDir, `${vehicle2.nation}-${vehicle2.tag}.png`);
|
|
2473
|
+
const info = await (await baker.bake(vehicle2)).png().toFile(outPath);
|
|
2474
|
+
console.log(`${outPath} \u2014 ${info.width}\xD7${info.height}px`);
|
|
2475
|
+
}
|
|
2476
|
+
function iconRenderCommand(app2) {
|
|
2477
|
+
return new Command16("render").description("Render a vehicle icon with its short name label composited over a type background").argument("[query]", "tank_id (number), tag, or short_name").option("--all", "render all vehicles").option("--color", "use color variant (default)").option("--clear", "use clear variant (no color background)").option("--bg <version>", "use pre-rendered background at given version").option("--pre-rendered-bg <version>", "alias for --bg").option("--to <dir>", "output directory (default: current working directory)").option("--create", "create output directory if it does not exist").action(async (query, options) => {
|
|
2478
|
+
try {
|
|
2479
|
+
if (!query && !options.all) {
|
|
2480
|
+
console.error("Provide a query argument or use --all to render all vehicles.");
|
|
2481
|
+
process.exit(1);
|
|
2482
|
+
}
|
|
2483
|
+
const outDir = options.to ?? process.cwd();
|
|
2484
|
+
if (options.to && !existsSync2(outDir)) {
|
|
2485
|
+
if (!options.create) {
|
|
2486
|
+
console.error(`Directory does not exist: ${outDir}
|
|
2487
|
+
Provide an existing path or add --create to create it.`);
|
|
2488
|
+
process.exit(1);
|
|
2489
|
+
}
|
|
2490
|
+
mkdirSync2(outDir, { recursive: true });
|
|
2491
|
+
}
|
|
2492
|
+
const bgVersion = options.bg ?? options.preRenderedBg ?? (options.clear ? "v2" : void 0);
|
|
2493
|
+
let builder;
|
|
2494
|
+
const useColor = !options.clear;
|
|
2495
|
+
if (bgVersion !== void 0) {
|
|
2496
|
+
const version2 = parseInt(bgVersion.replace(/\D+/g, ""), 10);
|
|
2497
|
+
if (version2 === 1) {
|
|
2498
|
+
builder = useColor ? new PogsColorV1() : new PogsClearV1();
|
|
2499
|
+
} else {
|
|
2500
|
+
builder = useColor ? new PogsColorV2() : new PogsClearV2();
|
|
2501
|
+
}
|
|
2502
|
+
} else {
|
|
2503
|
+
builder = useColor ? new PogsColor() : new PogsClear();
|
|
2504
|
+
}
|
|
2505
|
+
const vehicles = options.all ? await app2.getVehicles() : [await app2.findVehicle(query)];
|
|
2506
|
+
const bakers = Array.from({ length: CONCURRENCY }, () => builder.createBaker(app2));
|
|
2507
|
+
let idx = 0;
|
|
2508
|
+
await Promise.all(
|
|
2509
|
+
bakers.map(async (baker) => {
|
|
2510
|
+
while (idx < vehicles.length) {
|
|
2511
|
+
const vehicle2 = vehicles[idx++];
|
|
2512
|
+
await renderOneToFile(vehicle2, baker, outDir);
|
|
2513
|
+
}
|
|
2514
|
+
})
|
|
2515
|
+
);
|
|
2516
|
+
} catch (error) {
|
|
2517
|
+
if (error instanceof WGApiError) {
|
|
2518
|
+
console.error(`API error [${error.code}] ${error.field}: ${error.message}`);
|
|
2519
|
+
} else {
|
|
2520
|
+
console.error("Error:", error instanceof Error ? error.message : error);
|
|
2521
|
+
}
|
|
2522
|
+
process.exit(1);
|
|
2523
|
+
}
|
|
2524
|
+
});
|
|
2525
|
+
}
|
|
2526
|
+
|
|
2527
|
+
// src/commands/icon/fetch.ts
|
|
2528
|
+
import { Command as Command17 } from "commander";
|
|
2529
|
+
function parseSize(input) {
|
|
2530
|
+
switch (input.toLowerCase()) {
|
|
2531
|
+
case "s":
|
|
2532
|
+
case "small":
|
|
2533
|
+
return "small";
|
|
2534
|
+
case "m":
|
|
2535
|
+
case "medium":
|
|
2536
|
+
return "medium";
|
|
2537
|
+
case "l":
|
|
2538
|
+
case "large":
|
|
2539
|
+
return "large";
|
|
2540
|
+
case "xs":
|
|
2541
|
+
return "xs";
|
|
2542
|
+
default:
|
|
2543
|
+
throw new Error(`Invalid size "${input}". Use xs, s/small, m/medium, or l/large.`);
|
|
2544
|
+
}
|
|
2545
|
+
}
|
|
2546
|
+
function iconFetchCommand(app2) {
|
|
2547
|
+
return new Command17("fetch").description("Download vehicle icons into .data/icons/{size}/").argument("[query]", "tank_id (number), tag, or short_name \u2014 omit to fetch all").option("--size <size>", "icon size: s/small, m/medium (default), l/large", "medium").option("--all", "fetch icons for all vehicles").option("--force", "re-download icons that already exist locally").option("--concurrency <n>", "parallel downloads", "10").action(async (query, options) => {
|
|
2548
|
+
if (!query && !options.all) {
|
|
2549
|
+
console.error("Provide a query to fetch a single vehicle, or pass --all to fetch every vehicle.");
|
|
2550
|
+
process.exit(1);
|
|
2551
|
+
}
|
|
2552
|
+
try {
|
|
2553
|
+
await app2.fetchIcons({
|
|
2554
|
+
query,
|
|
2555
|
+
size: parseSize(options.size),
|
|
2556
|
+
force: options.force,
|
|
2557
|
+
concurrency: Math.max(1, parseInt(options.concurrency, 10) || 10)
|
|
2558
|
+
});
|
|
2559
|
+
} catch (error) {
|
|
2560
|
+
if (error instanceof WGApiError) {
|
|
2561
|
+
console.error(`API error [${error.code}] ${error.field}: ${error.message}`);
|
|
2562
|
+
} else {
|
|
2563
|
+
console.error("Error:", error instanceof Error ? error.message : error);
|
|
2564
|
+
}
|
|
2565
|
+
process.exit(1);
|
|
2566
|
+
}
|
|
2567
|
+
});
|
|
2568
|
+
}
|
|
2569
|
+
|
|
2570
|
+
// src/commands/icon/shrink.ts
|
|
2571
|
+
import { Command as Command18 } from "commander";
|
|
2572
|
+
function iconShrinkCommand(app2) {
|
|
2573
|
+
return new Command18("shrink").description("Fetch medium icons, trim transparent borders, save as xs in .data/icons/xs/").argument("[query]", "tank_id (number), tag, or short_name \u2014 omit to process all").option("--all", "process icons for all vehicles").option("--force", "re-process icons that are already cached").option("--concurrency <n>", "parallel downloads", "10").action(async (query, options) => {
|
|
2574
|
+
if (!query && !options.all) {
|
|
2575
|
+
console.error("Provide a query to shrink a single vehicle icon, or pass --all to process every vehicle.");
|
|
2576
|
+
process.exit(1);
|
|
2577
|
+
}
|
|
2578
|
+
try {
|
|
2579
|
+
await app2.fetchIcons({
|
|
2580
|
+
query,
|
|
2581
|
+
size: "xs",
|
|
2582
|
+
force: options.force ?? false,
|
|
2583
|
+
concurrency: Math.max(1, parseInt(options.concurrency, 10) || 10)
|
|
2584
|
+
});
|
|
2585
|
+
} catch (error) {
|
|
2586
|
+
if (error instanceof WGApiError) {
|
|
2587
|
+
console.error(`API error [${error.code}] ${error.field}: ${error.message}`);
|
|
2588
|
+
} else {
|
|
2589
|
+
console.error("Error:", error instanceof Error ? error.message : error);
|
|
2590
|
+
}
|
|
2591
|
+
process.exit(1);
|
|
2592
|
+
}
|
|
2593
|
+
});
|
|
2594
|
+
}
|
|
2595
|
+
|
|
2596
|
+
// src/lib/tomato-api.ts
|
|
2597
|
+
import { appendFile, mkdir as mkdir3, readFile as readFile7, stat, writeFile as writeFile8 } from "fs/promises";
|
|
2598
|
+
import { dirname as dirname4, join as join8, resolve as resolve5 } from "path";
|
|
2599
|
+
var HttpError = class extends Error {
|
|
2600
|
+
constructor(status, url) {
|
|
2601
|
+
super(`HTTP ${status} for ${url}`);
|
|
2602
|
+
this.status = status;
|
|
2603
|
+
}
|
|
2604
|
+
status;
|
|
2605
|
+
};
|
|
2606
|
+
var TomatoApi = class {
|
|
2607
|
+
BACKOFF_INTERVAL = 65 * 1e3;
|
|
2608
|
+
dataDir;
|
|
2609
|
+
apiKey;
|
|
2610
|
+
rateLimit;
|
|
2611
|
+
logPath;
|
|
2612
|
+
lastRequestAt = 0;
|
|
2613
|
+
draining = false;
|
|
2614
|
+
queue = [];
|
|
2615
|
+
constructor(dataDir = "./tomato/data", apiKey = "", rateLimit = 30) {
|
|
2616
|
+
this.dataDir = resolve5(findPkgRoot(new URL(import.meta.url)), dataDir);
|
|
2617
|
+
this.apiKey = apiKey || process.env.TOMATO_API_KEY || "";
|
|
2618
|
+
this.rateLimit = rateLimit;
|
|
2619
|
+
this.logPath = join8(this.dataDir, "requests.log");
|
|
2620
|
+
}
|
|
2621
|
+
async fetchVehicleVisuals(vehicleId, forceUpdate = false) {
|
|
2622
|
+
const filename = "index.json";
|
|
2623
|
+
const t0 = Date.now();
|
|
2624
|
+
if (!forceUpdate && await this.hasData(vehicleId, filename)) {
|
|
2625
|
+
return { vehicleId, fileName: filename, success: true, elapsed: Date.now() - t0, data: await this.loadData(vehicleId, filename) };
|
|
2626
|
+
}
|
|
2627
|
+
return new Promise((resolve8) => {
|
|
2628
|
+
this.queue.push(() => this.runVehicleVisuals(vehicleId, filename, resolve8));
|
|
2629
|
+
void this.drain();
|
|
2630
|
+
});
|
|
2631
|
+
}
|
|
2632
|
+
async fetchVehicleLoadouts(vehicleId, forceUpdate = false) {
|
|
2633
|
+
const filename = "loadouts.json";
|
|
2634
|
+
const t0 = Date.now();
|
|
2635
|
+
if (!forceUpdate && await this.hasData(vehicleId, filename)) {
|
|
2636
|
+
return { vehicleId, fileName: filename, success: true, elapsed: Date.now() - t0, data: await this.loadData(vehicleId, filename) };
|
|
2637
|
+
}
|
|
2638
|
+
return new Promise((resolve8) => {
|
|
2639
|
+
this.queue.push(() => this.runVehicleLoadouts(vehicleId, filename, resolve8));
|
|
2640
|
+
void this.drain();
|
|
2641
|
+
});
|
|
2642
|
+
}
|
|
2643
|
+
async fetchVehicleProLoadouts(vehicleId, forceUpdate = false) {
|
|
2644
|
+
const filename = "pro-loadouts.json";
|
|
2645
|
+
const t0 = Date.now();
|
|
2646
|
+
if (!forceUpdate && await this.hasData(vehicleId, filename)) {
|
|
2647
|
+
return { vehicleId, fileName: filename, success: true, elapsed: Date.now() - t0, data: await this.loadData(vehicleId, filename) };
|
|
2648
|
+
}
|
|
2649
|
+
return new Promise((resolve8) => {
|
|
2650
|
+
this.queue.push(() => this.runVehicleProLoadouts(vehicleId, filename, resolve8));
|
|
2651
|
+
void this.drain();
|
|
2652
|
+
});
|
|
2653
|
+
}
|
|
2654
|
+
async runVehicleVisuals(vehicleId, filename, resolve8) {
|
|
2655
|
+
const t0 = Date.now();
|
|
2656
|
+
const url = `https://tomato.gg/wot/vehicles/visuals/${vehicleId}.json`;
|
|
2657
|
+
try {
|
|
2658
|
+
const data = await this.request(url);
|
|
2659
|
+
await this.saveResponse(vehicleId, filename, data);
|
|
2660
|
+
resolve8({ vehicleId, fileName: filename, success: true, elapsed: Date.now() - t0, data });
|
|
2661
|
+
} catch (err) {
|
|
2662
|
+
resolve8({ vehicleId, fileName: filename, success: false, elapsed: Date.now() - t0, data: void 0, error: err instanceof Error ? err : new Error(String(err)) });
|
|
2663
|
+
throw err;
|
|
2664
|
+
}
|
|
2665
|
+
}
|
|
2666
|
+
async runVehicleLoadouts(vehicleId, filename, resolve8) {
|
|
2667
|
+
const t0 = Date.now();
|
|
2668
|
+
const url = `https://api.tomato.gg/api/tank/loadout-performance/${vehicleId}?cache=true`;
|
|
2669
|
+
try {
|
|
2670
|
+
const data = await this.request(url);
|
|
2671
|
+
await this.saveResponse(vehicleId, filename, data);
|
|
2672
|
+
resolve8({ vehicleId, fileName: filename, success: true, elapsed: Date.now() - t0, data });
|
|
2673
|
+
} catch (err) {
|
|
2674
|
+
resolve8({ vehicleId, fileName: filename, success: false, elapsed: Date.now() - t0, data: void 0, error: err instanceof Error ? err : new Error(String(err)) });
|
|
2675
|
+
throw err;
|
|
2676
|
+
}
|
|
2677
|
+
}
|
|
2678
|
+
async runVehicleProLoadouts(vehicleId, filename, resolve8) {
|
|
2679
|
+
const t0 = Date.now();
|
|
2680
|
+
const url = `https://api.tomato.gg/api/tank/top-loadouts/${vehicleId}?cache=true`;
|
|
2681
|
+
try {
|
|
2682
|
+
const data = await this.request(url);
|
|
2683
|
+
await this.saveResponse(vehicleId, filename, data);
|
|
2684
|
+
resolve8({ vehicleId, fileName: filename, success: true, elapsed: Date.now() - t0, data });
|
|
2685
|
+
} catch (err) {
|
|
2686
|
+
if (err instanceof HttpError && err.status === 404) {
|
|
2687
|
+
await this.saveResponse(vehicleId, filename, null);
|
|
2688
|
+
}
|
|
2689
|
+
resolve8({ vehicleId, fileName: filename, success: false, elapsed: Date.now() - t0, data: void 0, error: err instanceof Error ? err : new Error(String(err)) });
|
|
2690
|
+
throw err;
|
|
2691
|
+
}
|
|
2692
|
+
}
|
|
2693
|
+
async request(url) {
|
|
2694
|
+
const response = await fetch(url, {
|
|
2695
|
+
headers: { "x-api-key": this.apiKey },
|
|
2696
|
+
signal: AbortSignal.timeout(1e4)
|
|
2697
|
+
});
|
|
2698
|
+
await this.logRequest(url, response.status);
|
|
2699
|
+
if (!response.ok) {
|
|
2700
|
+
throw new HttpError(response.status, url);
|
|
2701
|
+
}
|
|
2702
|
+
return response.json();
|
|
2703
|
+
}
|
|
2704
|
+
async drain() {
|
|
2705
|
+
if (this.draining) {
|
|
2706
|
+
return;
|
|
2707
|
+
}
|
|
2708
|
+
this.draining = true;
|
|
2709
|
+
try {
|
|
2710
|
+
while (this.queue.length > 0) {
|
|
2711
|
+
await this.waitForRateLimit();
|
|
2712
|
+
this.lastRequestAt = Date.now();
|
|
2713
|
+
const task = this.queue.shift();
|
|
2714
|
+
try {
|
|
2715
|
+
await task();
|
|
2716
|
+
} catch (err) {
|
|
2717
|
+
if (err instanceof HttpError && err.status !== 404) {
|
|
2718
|
+
this.lastRequestAt += this.BACKOFF_INTERVAL;
|
|
2719
|
+
console.log(`Looks like rate/limit wall hit. Backing of for ${this.BACKOFF_INTERVAL / 1e3} sec`);
|
|
2720
|
+
}
|
|
2721
|
+
}
|
|
2722
|
+
}
|
|
2723
|
+
} finally {
|
|
2724
|
+
this.draining = false;
|
|
2725
|
+
}
|
|
2726
|
+
}
|
|
2727
|
+
async waitForRateLimit() {
|
|
2728
|
+
const intervalMs = Math.ceil(6e4 / this.rateLimit);
|
|
2729
|
+
const last = await this.getLastRequestTime();
|
|
2730
|
+
const waitMs = last + intervalMs - Date.now();
|
|
2731
|
+
if (waitMs > 0) {
|
|
2732
|
+
await new Promise((r) => setTimeout(r, waitMs));
|
|
2733
|
+
}
|
|
2734
|
+
}
|
|
2735
|
+
async getLastRequestTime() {
|
|
2736
|
+
let logMtime = 0;
|
|
2737
|
+
try {
|
|
2738
|
+
const s = await stat(this.logPath);
|
|
2739
|
+
logMtime = s.mtimeMs;
|
|
2740
|
+
} catch {
|
|
2741
|
+
}
|
|
2742
|
+
return Math.max(this.lastRequestAt, logMtime);
|
|
2743
|
+
}
|
|
2744
|
+
resolveFilePath(vehicleId, filename) {
|
|
2745
|
+
return join8(this.dataDir, String(vehicleId), filename);
|
|
2746
|
+
}
|
|
2747
|
+
async hasData(vehicleId, filename) {
|
|
2748
|
+
try {
|
|
2749
|
+
await stat(this.resolveFilePath(vehicleId, filename));
|
|
2750
|
+
return true;
|
|
2751
|
+
} catch {
|
|
2752
|
+
return false;
|
|
2753
|
+
}
|
|
2754
|
+
}
|
|
2755
|
+
async loadData(vehicleId, filename) {
|
|
2756
|
+
const content = await readFile7(this.resolveFilePath(vehicleId, filename), "utf-8");
|
|
2757
|
+
return JSON.parse(content);
|
|
2758
|
+
}
|
|
2759
|
+
async saveResponse(vehicleId, filename, data) {
|
|
2760
|
+
const filePath = this.resolveFilePath(vehicleId, filename);
|
|
2761
|
+
await mkdir3(dirname4(filePath), { recursive: true });
|
|
2762
|
+
await writeFile8(filePath, JSON.stringify(data, null, 2));
|
|
2763
|
+
}
|
|
2764
|
+
async logRequest(url, status) {
|
|
2765
|
+
await mkdir3(this.dataDir, { recursive: true });
|
|
2766
|
+
await appendFile(this.logPath, `${(/* @__PURE__ */ new Date()).toISOString()} ${url} ${status}
|
|
2767
|
+
`);
|
|
2768
|
+
}
|
|
2769
|
+
};
|
|
2770
|
+
|
|
2771
|
+
// src/commands/tomato/fetch.ts
|
|
2772
|
+
import { Command as Command19 } from "commander";
|
|
2773
|
+
|
|
2774
|
+
// src/lib/vehicle-type-helpers.ts
|
|
2775
|
+
var TYPE_MAP2 = {
|
|
2776
|
+
lightTank: "lt",
|
|
2777
|
+
mediumTank: "mt",
|
|
2778
|
+
heavyTank: "ht",
|
|
2779
|
+
"AT-SPG": "td",
|
|
2780
|
+
SPG: "at"
|
|
2781
|
+
};
|
|
2782
|
+
var ALIAS_MAP = Object.fromEntries(
|
|
2783
|
+
Object.entries(TYPE_MAP2).map(([type, alias]) => [alias, type])
|
|
2784
|
+
);
|
|
2785
|
+
function fromTypeAlias(alias) {
|
|
2786
|
+
const type = ALIAS_MAP[alias];
|
|
2787
|
+
if (!type) {
|
|
2788
|
+
throw new Error(`Unknown vehicle type alias: ${alias}`);
|
|
2789
|
+
}
|
|
2790
|
+
return type;
|
|
2791
|
+
}
|
|
2792
|
+
|
|
2793
|
+
// src/commands/tomato/fetch.ts
|
|
2794
|
+
var TYPE_ALIASES = ["lt", "mt", "ht", "td", "at"];
|
|
2795
|
+
async function fetchVehicle(tomato2, vehicle2) {
|
|
2796
|
+
return Promise.all([
|
|
2797
|
+
tomato2.fetchVehicleVisuals(vehicle2.tank_id),
|
|
2798
|
+
tomato2.fetchVehicleLoadouts(vehicle2.tank_id),
|
|
2799
|
+
tomato2.fetchVehicleProLoadouts(vehicle2.tank_id)
|
|
2800
|
+
]);
|
|
2801
|
+
}
|
|
2802
|
+
function vehicleLine(results, vehicle2, prefix = "") {
|
|
2803
|
+
const succeeded = results.filter((r) => r.success).length;
|
|
2804
|
+
const failed = results.filter((r) => !r.success).length;
|
|
2805
|
+
const elapsed = results.reduce((sum, r) => sum + r.elapsed, 0);
|
|
2806
|
+
const status = failed === 0 ? "OK" : succeeded === 0 ? "FAILED" : "PARTIAL";
|
|
2807
|
+
return `${prefix}${vehicle2.short_name} (${vehicle2.tank_id}): ${status} (${succeeded}/${failed}) ${elapsed}ms`;
|
|
2808
|
+
}
|
|
2809
|
+
function printErrors(results) {
|
|
2810
|
+
for (const r of results) {
|
|
2811
|
+
if (!r.success) {
|
|
2812
|
+
console.error(` error [${r.fileName}]: ${r.error?.message ?? "unknown"}`);
|
|
2813
|
+
}
|
|
2814
|
+
}
|
|
2815
|
+
}
|
|
2816
|
+
function printSummary(succeeded, failed) {
|
|
2817
|
+
const total = succeeded + failed;
|
|
2818
|
+
const summary = failed === 0 ? `${total} requests, ${succeeded} succeeded` : `${total} requests, ${succeeded} succeeded, ${failed} failed`;
|
|
2819
|
+
console.log(summary);
|
|
2820
|
+
}
|
|
2821
|
+
function tomatoFetchCommand(app2, tomato2) {
|
|
2822
|
+
return new Command19("fetch").description("Fetch Tomato.gg data for one vehicle or a filtered batch").argument("[query]", "tank_id (number), tag, or short_name").option("--tier <n>", "tier filter for batch mode", Number, 11).option("--lt", "include light tanks").option("--mt", "include medium tanks").option("--ht", "include heavy tanks").option("--td", "include tank destroyers (AT-SPG)").option("--at", "include SPGs (artillery)").action(async (query, options) => {
|
|
2823
|
+
try {
|
|
2824
|
+
if (query) {
|
|
2825
|
+
const vehicle2 = await app2.findVehicle(query);
|
|
2826
|
+
console.error(`Fetching data for ${vehicle2.short_name} (${vehicle2.tank_id})\u2026`);
|
|
2827
|
+
const results = await fetchVehicle(tomato2, vehicle2);
|
|
2828
|
+
printErrors(results);
|
|
2829
|
+
console.error(vehicleLine(results, vehicle2));
|
|
2830
|
+
const failed = results.filter((r) => !r.success).length;
|
|
2831
|
+
printSummary(results.length - failed, failed);
|
|
2832
|
+
if (failed > 0) {
|
|
2833
|
+
process.exit(1);
|
|
2834
|
+
}
|
|
2835
|
+
return;
|
|
2836
|
+
}
|
|
2837
|
+
const activeAliases = TYPE_ALIASES.filter((a) => Boolean(options[a]));
|
|
2838
|
+
if (activeAliases.length === 0) {
|
|
2839
|
+
console.error("Specify a <query> or --tier with at least one type flag (--lt, --mt, --ht, --td, --at).");
|
|
2840
|
+
process.exit(1);
|
|
2841
|
+
}
|
|
2842
|
+
const selectedTypes = new Set(activeAliases.map(fromTypeAlias));
|
|
2843
|
+
const tier = options.tier;
|
|
2844
|
+
const vehicles = await app2.getVehicles();
|
|
2845
|
+
const targets = vehicles.filter((v) => v.tier === tier && selectedTypes.has(v.type));
|
|
2846
|
+
if (targets.length === 0) {
|
|
2847
|
+
console.error(`No vehicles match tier ${tier} with the given type filters.`);
|
|
2848
|
+
process.exit(1);
|
|
2849
|
+
}
|
|
2850
|
+
console.error(`Fetching data for ${targets.length} vehicles (tier ${tier})\u2026`);
|
|
2851
|
+
let totalSucceeded = 0;
|
|
2852
|
+
let totalFailed = 0;
|
|
2853
|
+
const total = targets.length;
|
|
2854
|
+
const width = String(total).length;
|
|
2855
|
+
let idx = 0;
|
|
2856
|
+
for (const vehicle2 of targets) {
|
|
2857
|
+
idx++;
|
|
2858
|
+
const results = await fetchVehicle(tomato2, vehicle2);
|
|
2859
|
+
const succeeded = results.filter((r) => r.success).length;
|
|
2860
|
+
const failed = results.filter((r) => !r.success).length;
|
|
2861
|
+
totalSucceeded += succeeded;
|
|
2862
|
+
totalFailed += failed;
|
|
2863
|
+
const progress = `${String(idx).padStart(width)}/${total} `;
|
|
2864
|
+
console.error(vehicleLine(results, vehicle2, ` ${progress}`));
|
|
2865
|
+
printErrors(results);
|
|
2866
|
+
}
|
|
2867
|
+
printSummary(totalSucceeded, totalFailed);
|
|
2868
|
+
if (totalFailed > 0) {
|
|
2869
|
+
process.exit(1);
|
|
2870
|
+
}
|
|
2871
|
+
} catch (error) {
|
|
2872
|
+
console.error("Error:", error instanceof Error ? error.message : error);
|
|
2873
|
+
process.exit(1);
|
|
2874
|
+
}
|
|
2875
|
+
});
|
|
2876
|
+
}
|
|
2877
|
+
|
|
2878
|
+
// src/commands/bake/run.ts
|
|
2879
|
+
import { execFileSync } from "child_process";
|
|
2880
|
+
import { existsSync as existsSync3 } from "fs";
|
|
2881
|
+
import { resolve as resolve6 } from "path";
|
|
2882
|
+
import { Command as Command20 } from "commander";
|
|
2883
|
+
var SCRIPTS = {
|
|
2884
|
+
"clear": "bake-pogs-clear.sh",
|
|
2885
|
+
"color": "bake-pogs-color-dmg-fsr-vr-rld.sh",
|
|
2886
|
+
"simple": "bake-pogs-color-dmg-fsr-vr-rld.sh"
|
|
2887
|
+
};
|
|
2888
|
+
var scriptsDir = resolve6(findPkgRoot(new URL(import.meta.url)), "scripts");
|
|
2889
|
+
function bakeCommand() {
|
|
2890
|
+
const names = Object.keys(SCRIPTS).join(", ");
|
|
2891
|
+
return new Command20("bake").description("Run a bundled build script").argument("<script>", `available: ${names}`).allowUnknownOption(true).allowExcessArguments(true).passThroughOptions(true).action((scriptName, _options, cmd) => {
|
|
2892
|
+
const filename = SCRIPTS[scriptName];
|
|
2893
|
+
if (filename === void 0) {
|
|
2894
|
+
console.error(`Unknown script: ${scriptName}
|
|
2895
|
+
Available: ${names}`);
|
|
2896
|
+
process.exit(1);
|
|
2897
|
+
}
|
|
2898
|
+
const scriptPath = resolve6(scriptsDir, filename);
|
|
2899
|
+
if (!existsSync3(scriptPath)) {
|
|
2900
|
+
console.error(`Script not found: ${scriptPath}`);
|
|
2901
|
+
process.exit(1);
|
|
2902
|
+
}
|
|
2903
|
+
const extraArgs = cmd.args.slice(1);
|
|
2904
|
+
try {
|
|
2905
|
+
execFileSync("bash", [scriptPath, ...extraArgs], {
|
|
2906
|
+
stdio: "inherit",
|
|
2907
|
+
env: { ...process.env, PIE_WOT_CWD: process.cwd() }
|
|
2908
|
+
});
|
|
2909
|
+
} catch (err) {
|
|
2910
|
+
process.exit(err.status ?? 1);
|
|
2911
|
+
}
|
|
2912
|
+
});
|
|
2913
|
+
}
|
|
2914
|
+
|
|
2915
|
+
// src/index.ts
|
|
2916
|
+
var _version = "";
|
|
2917
|
+
var version = _version || "0.1.12";
|
|
2918
|
+
config({ path: resolve7(process.env.PIE_WOT_CWD ?? process.cwd(), ".env") });
|
|
2919
|
+
var app = new WGData();
|
|
2920
|
+
var atlasManager = new AtlasManager();
|
|
2921
|
+
var tomatoApi = new TomatoApi();
|
|
2922
|
+
var program = new Command21();
|
|
2923
|
+
program.name("pie-wot").description("CLI utilities for World of Tanks data and assets").version(version).enablePositionalOptions();
|
|
2924
|
+
var vehicle = new Command21("vehicle").description("WoT vehicle data");
|
|
2925
|
+
vehicle.addCommand(listVehiclesCommand(app));
|
|
2926
|
+
vehicle.addCommand(exportCommand(app));
|
|
2927
|
+
vehicle.addCommand(vehicleStatsCommand(app));
|
|
2928
|
+
vehicle.addCommand(bestConfigCommand(app));
|
|
2929
|
+
vehicle.addCommand(charsCommand(app));
|
|
2930
|
+
vehicle.addCommand(longAliasesCommand(app));
|
|
2931
|
+
var atlas = new Command21("atlas").description("Texture atlas tools");
|
|
2932
|
+
atlas.addCommand(inspectAtlasCommand(atlasManager));
|
|
2933
|
+
atlas.addCommand(pickCommand(atlasManager));
|
|
2934
|
+
atlas.addCommand(extractAtlasCommand(atlasManager));
|
|
2935
|
+
atlas.addCommand(packAtlasCommand(atlasManager));
|
|
2936
|
+
var font = new Command21("font").description("Pixel font tools");
|
|
2937
|
+
font.addCommand(renderCommand());
|
|
2938
|
+
var cache = new Command21("cache").description("API response cache");
|
|
2939
|
+
cache.addCommand(cachePurgeCommand(app));
|
|
2940
|
+
var dds = new Command21("dds").description("DDS texture tools");
|
|
2941
|
+
dds.addCommand(ddsDecodeCommand());
|
|
2942
|
+
dds.addCommand(ddsEncodeCommand());
|
|
2943
|
+
program.addCommand(vehicle);
|
|
2944
|
+
program.addCommand(atlas);
|
|
2945
|
+
program.addCommand(font);
|
|
2946
|
+
program.addCommand(cache);
|
|
2947
|
+
program.addCommand(dds);
|
|
2948
|
+
var icon = new Command21("icon").description("Vehicle icon generation tools");
|
|
2949
|
+
icon.addCommand(dumpBackgroundCommand());
|
|
2950
|
+
icon.addCommand(iconRenderCommand(app));
|
|
2951
|
+
icon.addCommand(iconFetchCommand(app));
|
|
2952
|
+
icon.addCommand(iconShrinkCommand(app));
|
|
2953
|
+
program.addCommand(icon);
|
|
2954
|
+
var tomato = new Command21("tomato").description("Tomato.gg data fetcher");
|
|
2955
|
+
tomato.addCommand(tomatoFetchCommand(app, tomatoApi));
|
|
2956
|
+
program.addCommand(tomato);
|
|
2957
|
+
program.addCommand(bakeCommand());
|
|
2958
|
+
await program.parseAsync();
|