@datamoc/mw_games 0.16.0 → 0.17.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +162 -0
- package/README.md +21 -1
- package/dist/ai/lua.d.ts.map +1 -1
- package/dist/ai/lua.js +1 -0
- package/dist/ai/lua.js.map +1 -1
- package/dist/core/ActionJournal.d.ts +5 -0
- package/dist/core/ActionJournal.d.ts.map +1 -1
- package/dist/core/ActionJournal.js +12 -1
- package/dist/core/ActionJournal.js.map +1 -1
- package/dist/core/Clone.d.ts +31 -0
- package/dist/core/Clone.d.ts.map +1 -0
- package/dist/core/Clone.js +84 -0
- package/dist/core/Clone.js.map +1 -0
- package/dist/core/Collection.d.ts +1 -0
- package/dist/core/Collection.d.ts.map +1 -1
- package/dist/core/Collection.js +6 -2
- package/dist/core/Collection.js.map +1 -1
- package/dist/core/HttpTransport.d.ts +17 -0
- package/dist/core/HttpTransport.d.ts.map +1 -1
- package/dist/core/HttpTransport.js +61 -0
- package/dist/core/HttpTransport.js.map +1 -1
- package/dist/core/Multiplayer.d.ts +9 -0
- package/dist/core/Multiplayer.d.ts.map +1 -1
- package/dist/core/Multiplayer.js +48 -4
- package/dist/core/Multiplayer.js.map +1 -1
- package/dist/core/News.d.ts.map +1 -1
- package/dist/core/News.js +1 -2
- package/dist/core/News.js.map +1 -1
- package/dist/core/Sanitize.d.ts +19 -0
- package/dist/core/Sanitize.d.ts.map +1 -1
- package/dist/core/Sanitize.js +37 -0
- package/dist/core/Sanitize.js.map +1 -1
- package/dist/core/Save.d.ts +18 -9
- package/dist/core/Save.d.ts.map +1 -1
- package/dist/core/Save.js +37 -21
- package/dist/core/Save.js.map +1 -1
- package/dist/core/SaveSync.js +4 -4
- package/dist/core/SaveSync.js.map +1 -1
- package/dist/core/Settings.d.ts.map +1 -1
- package/dist/core/Settings.js +2 -1
- package/dist/core/Settings.js.map +1 -1
- package/dist/core/State.d.ts.map +1 -1
- package/dist/core/State.js +8 -7
- package/dist/core/State.js.map +1 -1
- package/dist/core/StoredValue.d.ts +1 -1
- package/dist/core/StoredValue.d.ts.map +1 -1
- package/dist/core/StoredValue.js +3 -2
- package/dist/core/StoredValue.js.map +1 -1
- package/dist/core/Telemetry.d.ts +19 -1
- package/dist/core/Telemetry.d.ts.map +1 -1
- package/dist/core/Telemetry.js +26 -1
- package/dist/core/Telemetry.js.map +1 -1
- package/dist/core/index.d.ts +2 -1
- package/dist/core/index.d.ts.map +1 -1
- package/dist/core/index.js +2 -1
- package/dist/core/index.js.map +1 -1
- package/dist/mw_games.global.js +107 -107
- package/dist/mwl/MapFile.js +11 -1
- package/dist/mwl/MapFile.js.map +1 -1
- package/dist/mwl/expression.js +38 -5
- package/dist/mwl/expression.js.map +1 -1
- package/dist/mwl/fengari.d.ts +8 -0
- package/dist/mwl/fengari.d.ts.map +1 -1
- package/dist/mwl/fengari.js +72 -2
- package/dist/mwl/fengari.js.map +1 -1
- package/dist/mwl/persistence.d.ts.map +1 -1
- package/dist/mwl/persistence.js +2 -1
- package/dist/mwl/persistence.js.map +1 -1
- package/dist/mwl/runtime.d.ts.map +1 -1
- package/dist/mwl/runtime.js +6 -1
- package/dist/mwl/runtime.js.map +1 -1
- package/dist/rpg/Marshal.d.ts.map +1 -1
- package/dist/rpg/Marshal.js +13 -0
- package/dist/rpg/Marshal.js.map +1 -1
- package/dist/simulation/Campaign.d.ts.map +1 -1
- package/dist/simulation/Campaign.js +9 -5
- package/dist/simulation/Campaign.js.map +1 -1
- package/dist/simulation/Harness.d.ts.map +1 -1
- package/dist/simulation/Harness.js +2 -1
- package/dist/simulation/Harness.js.map +1 -1
- package/dist/simulation/Runtime.d.ts +10 -0
- package/dist/simulation/Runtime.d.ts.map +1 -1
- package/dist/simulation/Runtime.js +25 -4
- package/dist/simulation/Runtime.js.map +1 -1
- package/dist/testing/index.d.ts +105 -0
- package/dist/testing/index.d.ts.map +1 -0
- package/dist/testing/index.js +134 -0
- package/dist/testing/index.js.map +1 -0
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/package.json +90 -25
- package/sbom.cdx.json +5589 -0
- package/tools/benchmark-browser.d.mts +33 -0
- package/tools/benchmark-browser.mjs +163 -0
- package/tools/benchmark-history.d.mts +8 -0
- package/tools/benchmark-history.mjs +31 -0
- package/tools/browser-smoke.d.mts +30 -0
- package/tools/browser-smoke.mjs +167 -0
- package/tools/bundle-size.d.mts +11 -0
- package/tools/bundle-size.mjs +138 -0
- package/tools/csp.d.mts +7 -0
- package/tools/csp.mjs +65 -0
- package/tools/emit-page.d.mts +44 -0
- package/tools/emit-page.mjs +188 -0
- package/tools/extract-rgssad.d.mts +5 -0
- package/tools/extract-rgssad.mjs +64 -0
- package/tools/find-chrome.d.mts +7 -0
- package/tools/find-chrome.mjs +104 -0
- package/tools/i18n-edit.d.mts +29 -0
- package/tools/i18n-edit.mjs +748 -0
- package/tools/make-example-assets.d.mts +2 -0
- package/tools/make-example-assets.mjs +727 -0
- package/tools/multiplayer-server.d.mts +27 -0
- package/tools/multiplayer-server.mjs +195 -0
- package/tools/mwl.mjs +1 -0
- package/tools/sbom.d.mts +83 -0
- package/tools/sbom.mjs +332 -0
- package/tools/vendored/rgssad-wasm/LICENSE-MIT +21 -0
- package/tools/vendored/rgssad-wasm/README.md +15 -0
- package/tools/vendored/rgssad-wasm/package.json +17 -0
- package/tools/vendored/rgssad-wasm/rgssad_wasm.d.ts +41 -0
- package/tools/vendored/rgssad-wasm/rgssad_wasm.js +6 -0
- package/tools/vendored/rgssad-wasm/rgssad_wasm_bg.js +399 -0
- package/tools/vendored/rgssad-wasm/rgssad_wasm_bg.wasm +0 -0
- package/tools/vendored/rgssad-wasm/rgssad_wasm_bg.wasm.d.ts +17 -0
- package/tools/vite.d.mts +17 -0
- package/tools/vite.mjs +83 -0
|
@@ -0,0 +1,727 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { deflateSync } from 'node:zlib';
|
|
3
|
+
import { existsSync, realpathSync } from 'node:fs';
|
|
4
|
+
import { writeFile, mkdir } from 'node:fs/promises';
|
|
5
|
+
import { join, dirname, resolve } from 'node:path';
|
|
6
|
+
import { fileURLToPath } from 'node:url';
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Generates the art and sound the examples use.
|
|
10
|
+
*
|
|
11
|
+
* These are drawn and synthesised here rather than downloaded, for one reason: everything
|
|
12
|
+
* `mwg` ships has to be redistributable under its own licence. Borrowing a tileset means
|
|
13
|
+
* inheriting whatever that tileset's terms are, and game art is exactly where those terms
|
|
14
|
+
* bite. Generated assets have no such history.
|
|
15
|
+
*
|
|
16
|
+
* They are deliberately plain. If you want real pixel art in your own game, Kenney
|
|
17
|
+
* (kenney.nl) publishes large tilesets under CC0, which imposes nothing.
|
|
18
|
+
*/
|
|
19
|
+
|
|
20
|
+
const here = dirname(fileURLToPath(import.meta.url));
|
|
21
|
+
//this repository's examples read their assets from here; an installed package has no examples
|
|
22
|
+
const EXAMPLE_ASSETS = join(here, '..', 'examples', 'assets');
|
|
23
|
+
|
|
24
|
+
const TILE = 16;
|
|
25
|
+
const COLUMNS = 8;
|
|
26
|
+
const ROWS = 2;
|
|
27
|
+
|
|
28
|
+
// ---------------------------------------------------------------- PNG encoding
|
|
29
|
+
|
|
30
|
+
function crc32(buf) {
|
|
31
|
+
let crc = ~0;
|
|
32
|
+
for (const byte of buf) {
|
|
33
|
+
crc ^= byte;
|
|
34
|
+
for (let i = 0; i < 8; i++) crc = (crc >>> 1) ^ (0xedb88320 & -(crc & 1));
|
|
35
|
+
}
|
|
36
|
+
return ~crc >>> 0;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function chunk(type, data) {
|
|
40
|
+
const head = Buffer.alloc(8);
|
|
41
|
+
head.writeUInt32BE(data.length, 0);
|
|
42
|
+
head.write(type, 4, 'ascii');
|
|
43
|
+
|
|
44
|
+
const crcInput = Buffer.concat([Buffer.from(type, 'ascii'), data]);
|
|
45
|
+
const tail = Buffer.alloc(4);
|
|
46
|
+
tail.writeUInt32BE(crc32(crcInput), 0);
|
|
47
|
+
|
|
48
|
+
return Buffer.concat([head, data, tail]);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/** @param pixels RGBA bytes, row-major */
|
|
52
|
+
function encodePng(width, height, pixels) {
|
|
53
|
+
//each scanline is prefixed with its filter type; 0 means "no filter"
|
|
54
|
+
const raw = Buffer.alloc(height * (1 + width * 4));
|
|
55
|
+
for (let y = 0; y < height; y++) {
|
|
56
|
+
const to = y * (1 + width * 4);
|
|
57
|
+
raw[to] = 0;
|
|
58
|
+
pixels.copy(raw, to + 1, y * width * 4, (y + 1) * width * 4);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
const ihdr = Buffer.alloc(13);
|
|
62
|
+
ihdr.writeUInt32BE(width, 0);
|
|
63
|
+
ihdr.writeUInt32BE(height, 4);
|
|
64
|
+
ihdr[8] = 8; //bit depth
|
|
65
|
+
ihdr[9] = 6; //colour type: RGBA
|
|
66
|
+
//10..12 stay zero: deflate, adaptive filtering, no interlace
|
|
67
|
+
|
|
68
|
+
return Buffer.concat([
|
|
69
|
+
Buffer.from([0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a]),
|
|
70
|
+
chunk('IHDR', ihdr),
|
|
71
|
+
chunk('IDAT', deflateSync(raw, { level: 9 })),
|
|
72
|
+
chunk('IEND', Buffer.alloc(0)),
|
|
73
|
+
]);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
// ---------------------------------------------------------------- the tileset
|
|
77
|
+
|
|
78
|
+
class Canvas {
|
|
79
|
+
constructor(width, height) {
|
|
80
|
+
this.width = width;
|
|
81
|
+
this.height = height;
|
|
82
|
+
this.data = Buffer.alloc(width * height * 4);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
set(x, y, [r, g, b, a = 255]) {
|
|
86
|
+
if (x < 0 || y < 0 || x >= this.width || y >= this.height) return;
|
|
87
|
+
const i = (y * this.width + x) * 4;
|
|
88
|
+
this.data[i] = r;
|
|
89
|
+
this.data[i + 1] = g;
|
|
90
|
+
this.data[i + 2] = b;
|
|
91
|
+
this.data[i + 3] = a;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
rect(x, y, w, h, color) {
|
|
95
|
+
for (let dy = 0; dy < h; dy++) {
|
|
96
|
+
for (let dx = 0; dx < w; dx++) this.set(x + dx, y + dy, color);
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
border(x, y, w, h, color) {
|
|
101
|
+
for (let dx = 0; dx < w; dx++) {
|
|
102
|
+
this.set(x + dx, y, color);
|
|
103
|
+
this.set(x + dx, y + h - 1, color);
|
|
104
|
+
}
|
|
105
|
+
for (let dy = 0; dy < h; dy++) {
|
|
106
|
+
this.set(x, y + dy, color);
|
|
107
|
+
this.set(x + w - 1, y + dy, color);
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/** a filled circle, which is all the "creature" shapes need */
|
|
112
|
+
disc(cx, cy, radius, color) {
|
|
113
|
+
for (let dy = -radius; dy <= radius; dy++) {
|
|
114
|
+
for (let dx = -radius; dx <= radius; dx++) {
|
|
115
|
+
if (dx * dx + dy * dy <= radius * radius) this.set(cx + dx, cy + dy, color);
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/**
|
|
122
|
+
* A deterministic hash, so the speckling on the floor tiles is the same every build and
|
|
123
|
+
* the generated file does not churn in git.
|
|
124
|
+
*/
|
|
125
|
+
function noise(x, y, salt) {
|
|
126
|
+
let h = Math.imul(x * 374761393 + y * 668265263 + salt * 1442695041, 2246822519);
|
|
127
|
+
h = (h ^ (h >>> 13)) >>> 0;
|
|
128
|
+
return (h % 1000) / 1000;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
const PALETTE = {
|
|
132
|
+
stone: [96, 96, 104],
|
|
133
|
+
stoneDark: [72, 72, 80],
|
|
134
|
+
stoneLight: [124, 124, 132],
|
|
135
|
+
wall: [58, 54, 62],
|
|
136
|
+
wallTop: [86, 80, 92],
|
|
137
|
+
water: [56, 88, 148],
|
|
138
|
+
waterLight: [78, 118, 184],
|
|
139
|
+
grass: [78, 116, 66],
|
|
140
|
+
door: [132, 96, 52],
|
|
141
|
+
gold: [214, 178, 66],
|
|
142
|
+
hero: [232, 224, 208],
|
|
143
|
+
heroDark: [176, 168, 152],
|
|
144
|
+
rat: [148, 112, 88],
|
|
145
|
+
blob: [116, 168, 92],
|
|
146
|
+
spike: [156, 156, 164],
|
|
147
|
+
rust: [168, 64, 48],
|
|
148
|
+
};
|
|
149
|
+
|
|
150
|
+
/** the tile index each name sits at, exported so the examples do not use magic numbers */
|
|
151
|
+
const TILES = {
|
|
152
|
+
FLOOR: 0,
|
|
153
|
+
FLOOR_WORN: 1,
|
|
154
|
+
WALL: 2,
|
|
155
|
+
WALL_TOP: 3,
|
|
156
|
+
WATER: 4,
|
|
157
|
+
GRASS: 5,
|
|
158
|
+
DOOR: 6,
|
|
159
|
+
COIN: 7,
|
|
160
|
+
HERO: 8,
|
|
161
|
+
RAT: 9,
|
|
162
|
+
BLOB: 10,
|
|
163
|
+
EMPTY: 11,
|
|
164
|
+
TRAP: 12,
|
|
165
|
+
};
|
|
166
|
+
|
|
167
|
+
function drawTile(canvas, index, draw) {
|
|
168
|
+
const x = (index % COLUMNS) * TILE;
|
|
169
|
+
const y = Math.floor(index / COLUMNS) * TILE;
|
|
170
|
+
draw(x, y);
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
function buildTileset() {
|
|
174
|
+
const canvas = new Canvas(COLUMNS * TILE, ROWS * TILE);
|
|
175
|
+
|
|
176
|
+
//floor: flat stone with a little deterministic speckle so tiling is visible
|
|
177
|
+
drawTile(canvas, TILES.FLOOR, (ox, oy) => {
|
|
178
|
+
canvas.rect(ox, oy, TILE, TILE, PALETTE.stone);
|
|
179
|
+
for (let y = 0; y < TILE; y++) {
|
|
180
|
+
for (let x = 0; x < TILE; x++) {
|
|
181
|
+
const n = noise(x, y, 1);
|
|
182
|
+
if (n > 0.88) canvas.set(ox + x, oy + y, PALETTE.stoneLight);
|
|
183
|
+
else if (n < 0.12) canvas.set(ox + x, oy + y, PALETTE.stoneDark);
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
});
|
|
187
|
+
|
|
188
|
+
//a second floor variant, so a map does not look like graph paper
|
|
189
|
+
drawTile(canvas, TILES.FLOOR_WORN, (ox, oy) => {
|
|
190
|
+
canvas.rect(ox, oy, TILE, TILE, PALETTE.stone);
|
|
191
|
+
for (let y = 0; y < TILE; y++) {
|
|
192
|
+
for (let x = 0; x < TILE; x++) {
|
|
193
|
+
const n = noise(x, y, 7);
|
|
194
|
+
if (n > 0.72) canvas.set(ox + x, oy + y, PALETTE.stoneDark);
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
canvas.rect(ox + 3, oy + 9, 5, 1, PALETTE.stoneDark);
|
|
198
|
+
canvas.rect(ox + 10, oy + 4, 3, 1, PALETTE.stoneDark);
|
|
199
|
+
});
|
|
200
|
+
|
|
201
|
+
//wall: brickwork, offset every other course
|
|
202
|
+
drawTile(canvas, TILES.WALL, (ox, oy) => {
|
|
203
|
+
canvas.rect(ox, oy, TILE, TILE, PALETTE.wall);
|
|
204
|
+
for (let row = 0; row < 4; row++) {
|
|
205
|
+
const y = oy + row * 4;
|
|
206
|
+
canvas.rect(ox, y, TILE, 1, PALETTE.wallTop);
|
|
207
|
+
const offset = row % 2 === 0 ? 0 : 8;
|
|
208
|
+
for (let brick = 0; brick < 2; brick++) {
|
|
209
|
+
const x = ox + ((offset + brick * 8) % TILE);
|
|
210
|
+
canvas.rect(x, y, 1, 4, PALETTE.wallTop);
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
});
|
|
214
|
+
|
|
215
|
+
drawTile(canvas, TILES.WALL_TOP, (ox, oy) => {
|
|
216
|
+
canvas.rect(ox, oy, TILE, TILE, PALETTE.wallTop);
|
|
217
|
+
canvas.border(ox, oy, TILE, TILE, PALETTE.wall);
|
|
218
|
+
});
|
|
219
|
+
|
|
220
|
+
drawTile(canvas, TILES.WATER, (ox, oy) => {
|
|
221
|
+
canvas.rect(ox, oy, TILE, TILE, PALETTE.water);
|
|
222
|
+
for (let y = 0; y < TILE; y++) {
|
|
223
|
+
for (let x = 0; x < TILE; x++) {
|
|
224
|
+
if (noise(x, y, 3) > 0.8) canvas.set(ox + x, oy + y, PALETTE.waterLight);
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
});
|
|
228
|
+
|
|
229
|
+
drawTile(canvas, TILES.GRASS, (ox, oy) => {
|
|
230
|
+
canvas.rect(ox, oy, TILE, TILE, PALETTE.stone);
|
|
231
|
+
for (let x = 0; x < TILE; x++) {
|
|
232
|
+
const height = 3 + Math.floor(noise(x, 0, 5) * 5);
|
|
233
|
+
for (let y = 0; y < height; y++) {
|
|
234
|
+
canvas.set(ox + x, oy + TILE - 1 - y, PALETTE.grass);
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
});
|
|
238
|
+
|
|
239
|
+
drawTile(canvas, TILES.DOOR, (ox, oy) => {
|
|
240
|
+
canvas.rect(ox, oy, TILE, TILE, PALETTE.wall);
|
|
241
|
+
canvas.rect(ox + 3, oy + 2, 10, 14, PALETTE.door);
|
|
242
|
+
canvas.border(ox + 3, oy + 2, 10, 14, PALETTE.wallTop);
|
|
243
|
+
canvas.rect(ox + 10, oy + 9, 1, 2, PALETTE.gold);
|
|
244
|
+
});
|
|
245
|
+
|
|
246
|
+
drawTile(canvas, TILES.COIN, (ox, oy) => {
|
|
247
|
+
canvas.disc(ox + 8, oy + 9, 4, PALETTE.gold);
|
|
248
|
+
canvas.disc(ox + 7, oy + 8, 2, [240, 214, 120]);
|
|
249
|
+
});
|
|
250
|
+
|
|
251
|
+
//the "creatures" are simple on purpose: what matters is that they take a colour well
|
|
252
|
+
drawTile(canvas, TILES.HERO, (ox, oy) => {
|
|
253
|
+
canvas.disc(ox + 8, oy + 5, 3, PALETTE.hero);
|
|
254
|
+
canvas.rect(ox + 5, oy + 8, 6, 6, PALETTE.hero);
|
|
255
|
+
canvas.rect(ox + 5, oy + 12, 6, 2, PALETTE.heroDark);
|
|
256
|
+
canvas.set(ox + 7, oy + 5, [40, 40, 48]);
|
|
257
|
+
canvas.set(ox + 9, oy + 5, [40, 40, 48]);
|
|
258
|
+
});
|
|
259
|
+
|
|
260
|
+
drawTile(canvas, TILES.RAT, (ox, oy) => {
|
|
261
|
+
canvas.rect(ox + 3, oy + 8, 9, 5, PALETTE.rat);
|
|
262
|
+
canvas.disc(ox + 12, oy + 9, 2, PALETTE.rat);
|
|
263
|
+
canvas.rect(ox + 1, oy + 10, 3, 1, PALETTE.rat);
|
|
264
|
+
canvas.set(ox + 13, oy + 8, [30, 20, 20]);
|
|
265
|
+
});
|
|
266
|
+
|
|
267
|
+
drawTile(canvas, TILES.BLOB, (ox, oy) => {
|
|
268
|
+
canvas.disc(ox + 8, oy + 10, 5, PALETTE.blob);
|
|
269
|
+
canvas.disc(ox + 6, oy + 8, 2, [150, 200, 120]);
|
|
270
|
+
});
|
|
271
|
+
|
|
272
|
+
//a sprung trap: the same floor a hidden one disguises itself as, plus the spikes a
|
|
273
|
+
//revealed one shows once discovered
|
|
274
|
+
drawTile(canvas, TILES.TRAP, (ox, oy) => {
|
|
275
|
+
canvas.rect(ox, oy, TILE, TILE, PALETTE.stone);
|
|
276
|
+
for (let y = 0; y < TILE; y++) {
|
|
277
|
+
for (let x = 0; x < TILE; x++) {
|
|
278
|
+
const n = noise(x, y, 1);
|
|
279
|
+
if (n > 0.88) canvas.set(ox + x, oy + y, PALETTE.stoneLight);
|
|
280
|
+
else if (n < 0.12) canvas.set(ox + x, oy + y, PALETTE.stoneDark);
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
for (const sx of [3, 7, 11]) {
|
|
284
|
+
for (let row = 0; row < 6; row++) {
|
|
285
|
+
const width = 6 - row;
|
|
286
|
+
canvas.rect(ox + sx - Math.floor(width / 2), oy + 13 - row, width, 1, PALETTE.spike);
|
|
287
|
+
}
|
|
288
|
+
canvas.set(ox + sx, oy + 7, PALETTE.rust);
|
|
289
|
+
}
|
|
290
|
+
});
|
|
291
|
+
|
|
292
|
+
//TILES.EMPTY is left fully transparent on purpose
|
|
293
|
+
|
|
294
|
+
return encodePng(canvas.width, canvas.height, canvas.data);
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
// ------------------------------------------------- backdrop and characters
|
|
298
|
+
|
|
299
|
+
/**
|
|
300
|
+
* A backdrop for the dialogue example.
|
|
301
|
+
*
|
|
302
|
+
* Crude on purpose: a wall, a floor, a window with light falling from it. Enough for a
|
|
303
|
+
* character to stand in front of and for a scene change to be visible.
|
|
304
|
+
*/
|
|
305
|
+
function buildBackdrop(palette) {
|
|
306
|
+
const width = 320;
|
|
307
|
+
const height = 180;
|
|
308
|
+
const canvas = new Canvas(width, height);
|
|
309
|
+
const horizon = Math.round(height * 0.62);
|
|
310
|
+
|
|
311
|
+
//wall, shaded slightly darker towards the floor
|
|
312
|
+
for (let y = 0; y < horizon; y++) {
|
|
313
|
+
const shade = 1 - (y / horizon) * 0.25;
|
|
314
|
+
canvas.rect(
|
|
315
|
+
0,
|
|
316
|
+
y,
|
|
317
|
+
width,
|
|
318
|
+
1,
|
|
319
|
+
palette.wall.map((c) => Math.round(c * shade)),
|
|
320
|
+
);
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
//floor, in perspective-ish bands that get shorter towards the horizon
|
|
324
|
+
for (let y = horizon; y < height; y++) {
|
|
325
|
+
const depth = (y - horizon) / (height - horizon);
|
|
326
|
+
const shade = 0.55 + depth * 0.45;
|
|
327
|
+
canvas.rect(
|
|
328
|
+
0,
|
|
329
|
+
y,
|
|
330
|
+
width,
|
|
331
|
+
1,
|
|
332
|
+
palette.floor.map((c) => Math.round(c * shade)),
|
|
333
|
+
);
|
|
334
|
+
}
|
|
335
|
+
for (let x = 0; x < width; x += 24) {
|
|
336
|
+
for (let y = horizon; y < height; y++) {
|
|
337
|
+
if (noise(x, y, 21) > 0.7) canvas.set(x, y, palette.floorLine);
|
|
338
|
+
}
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
//a window, and the light it throws on the floor
|
|
342
|
+
const wx = 210;
|
|
343
|
+
const wy = 24;
|
|
344
|
+
canvas.rect(wx, wy, 64, 56, palette.sky);
|
|
345
|
+
canvas.border(wx - 2, wy - 2, 68, 60, palette.frame);
|
|
346
|
+
canvas.rect(wx + 30, wy, 2, 56, palette.frame);
|
|
347
|
+
canvas.rect(wx, wy + 26, 64, 2, palette.frame);
|
|
348
|
+
|
|
349
|
+
for (let y = horizon; y < height; y++) {
|
|
350
|
+
const spread = (y - horizon) * 0.7;
|
|
351
|
+
const left = Math.round(wx - spread * 0.4);
|
|
352
|
+
const width_ = Math.round(64 + spread);
|
|
353
|
+
for (let x = left; x < left + width_; x++) {
|
|
354
|
+
if (x < 0 || x >= 320) continue;
|
|
355
|
+
const i = (y * 320 + x) * 4;
|
|
356
|
+
//lighten what is already there rather than painting over it
|
|
357
|
+
canvas.data[i] = Math.min(255, canvas.data[i] + 26);
|
|
358
|
+
canvas.data[i + 1] = Math.min(255, canvas.data[i + 1] + 24);
|
|
359
|
+
canvas.data[i + 2] = Math.min(255, canvas.data[i + 2] + 14);
|
|
360
|
+
}
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
return encodePng(width, height, canvas.data);
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
/**
|
|
367
|
+
* Character parts, drawn one at a time.
|
|
368
|
+
*
|
|
369
|
+
* The figure is decomposed so that every surface a game would want to vary is its own
|
|
370
|
+
* colour: skin, eyes, hair, the upper garment and the lower one. Two characters are then
|
|
371
|
+
* two palettes over the same silhouette, which is how a real game gets a townful of people
|
|
372
|
+
* out of one drawing, and how a player-made character works at all.
|
|
373
|
+
*
|
|
374
|
+
* Frames are 64x96, standing on the bottom edge, and only the face differs between them.
|
|
375
|
+
*/
|
|
376
|
+
const CHARACTER_FRAME = { width: 64, height: 96 };
|
|
377
|
+
|
|
378
|
+
/** the expression each frame carries, in the order they are drawn */
|
|
379
|
+
const EXPRESSIONS = ['neutral', 'happy', 'cross'];
|
|
380
|
+
|
|
381
|
+
function shade(color, factor) {
|
|
382
|
+
return color.map((c) => Math.max(0, Math.min(255, Math.round(c * factor))));
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
const PARTS = {
|
|
386
|
+
/** trousers or skirt, plus the shoes below them */
|
|
387
|
+
lower(canvas, ox, palette) {
|
|
388
|
+
canvas.rect(ox + 20, 68, 24, 20, palette.bottom);
|
|
389
|
+
//a gap between the legs, so the silhouette does not read as a single block
|
|
390
|
+
canvas.rect(ox + 31, 76, 2, 12, shade(palette.bottom, 0.6));
|
|
391
|
+
canvas.rect(ox + 19, 88, 11, 5, shade(palette.bottom, 0.45));
|
|
392
|
+
canvas.rect(ox + 34, 88, 11, 5, shade(palette.bottom, 0.45));
|
|
393
|
+
},
|
|
394
|
+
|
|
395
|
+
/** shirt and sleeves; the hands are drawn after, so they sit on top of the cuffs */
|
|
396
|
+
upper(canvas, ox, palette) {
|
|
397
|
+
canvas.rect(ox + 19, 43, 26, 26, palette.top);
|
|
398
|
+
canvas.rect(ox + 14, 45, 5, 20, shade(palette.top, 0.85));
|
|
399
|
+
canvas.rect(ox + 45, 45, 5, 20, shade(palette.top, 0.85));
|
|
400
|
+
//a collar, which is what stops the head looking stuck onto a rectangle
|
|
401
|
+
canvas.rect(ox + 27, 43, 10, 3, shade(palette.top, 0.7));
|
|
402
|
+
},
|
|
403
|
+
|
|
404
|
+
hands(canvas, ox, palette) {
|
|
405
|
+
canvas.rect(ox + 14, 65, 5, 5, palette.skin);
|
|
406
|
+
canvas.rect(ox + 45, 65, 5, 5, palette.skin);
|
|
407
|
+
},
|
|
408
|
+
|
|
409
|
+
head(canvas, ox, palette) {
|
|
410
|
+
//neck first, so the jaw overlaps it
|
|
411
|
+
canvas.rect(ox + 28, 40, 8, 5, shade(palette.skin, 0.85));
|
|
412
|
+
canvas.disc(ox + 32, 30, 13, palette.skin);
|
|
413
|
+
},
|
|
414
|
+
|
|
415
|
+
hair(canvas, ox, palette) {
|
|
416
|
+
canvas.disc(ox + 32, 22, 13, palette.hair);
|
|
417
|
+
canvas.rect(ox + 19, 22, 26, 4, palette.hair);
|
|
418
|
+
//sideburns, framing the face
|
|
419
|
+
canvas.rect(ox + 19, 26, 3, 10, palette.hair);
|
|
420
|
+
canvas.rect(ox + 42, 26, 3, 10, palette.hair);
|
|
421
|
+
},
|
|
422
|
+
|
|
423
|
+
/** the eyes carry their own colour, with a dark pupil so they read at this size */
|
|
424
|
+
face(canvas, ox, palette, expression) {
|
|
425
|
+
const dark = [28, 24, 30];
|
|
426
|
+
const mouth = shade(palette.skin, 0.55);
|
|
427
|
+
|
|
428
|
+
if (expression === 'happy') {
|
|
429
|
+
//closed, curved-up eyes: no iris is visible, so no eye colour here
|
|
430
|
+
canvas.rect(ox + 25, 30, 4, 1, dark);
|
|
431
|
+
canvas.set(ox + 24, 31, dark);
|
|
432
|
+
canvas.set(ox + 29, 31, dark);
|
|
433
|
+
canvas.rect(ox + 35, 30, 4, 1, dark);
|
|
434
|
+
canvas.set(ox + 34, 31, dark);
|
|
435
|
+
canvas.set(ox + 39, 31, dark);
|
|
436
|
+
|
|
437
|
+
canvas.rect(ox + 28, 36, 8, 1, mouth);
|
|
438
|
+
canvas.set(ox + 27, 35, mouth);
|
|
439
|
+
canvas.set(ox + 36, 35, mouth);
|
|
440
|
+
return;
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
//open eyes: white, iris, pupil
|
|
444
|
+
for (const x of [25, 34]) {
|
|
445
|
+
canvas.rect(ox + x, 28, 5, 4, [242, 240, 238]);
|
|
446
|
+
canvas.rect(ox + x + 1, 29, 3, 3, palette.eyes);
|
|
447
|
+
canvas.rect(ox + x + 2, 30, 1, 2, dark);
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
if (expression === 'cross') {
|
|
451
|
+
//brows angled inwards, and a flat mouth
|
|
452
|
+
canvas.rect(ox + 24, 25, 5, 1, palette.hair);
|
|
453
|
+
canvas.set(ox + 29, 26, palette.hair);
|
|
454
|
+
canvas.rect(ox + 35, 25, 5, 1, palette.hair);
|
|
455
|
+
canvas.set(ox + 34, 26, palette.hair);
|
|
456
|
+
canvas.rect(ox + 29, 37, 6, 1, mouth);
|
|
457
|
+
} else {
|
|
458
|
+
canvas.rect(ox + 24, 24, 5, 1, palette.hair);
|
|
459
|
+
canvas.rect(ox + 35, 24, 5, 1, palette.hair);
|
|
460
|
+
canvas.rect(ox + 29, 36, 6, 1, mouth);
|
|
461
|
+
}
|
|
462
|
+
},
|
|
463
|
+
};
|
|
464
|
+
|
|
465
|
+
/**
|
|
466
|
+
* Builds one character sheet from a five-colour palette.
|
|
467
|
+
*
|
|
468
|
+
* @param palette skin, eyes, hair, top, bottom, each an [r, g, b] triple
|
|
469
|
+
*/
|
|
470
|
+
function buildCharacter(palette) {
|
|
471
|
+
const { width, height } = CHARACTER_FRAME;
|
|
472
|
+
const canvas = new Canvas(width * EXPRESSIONS.length, height);
|
|
473
|
+
|
|
474
|
+
EXPRESSIONS.forEach((expression, index) => {
|
|
475
|
+
const ox = index * width;
|
|
476
|
+
|
|
477
|
+
//back to front, so each part overlaps the one before it correctly
|
|
478
|
+
PARTS.lower(canvas, ox, palette);
|
|
479
|
+
PARTS.upper(canvas, ox, palette);
|
|
480
|
+
PARTS.hands(canvas, ox, palette);
|
|
481
|
+
PARTS.head(canvas, ox, palette);
|
|
482
|
+
PARTS.hair(canvas, ox, palette);
|
|
483
|
+
PARTS.face(canvas, ox, palette, expression);
|
|
484
|
+
});
|
|
485
|
+
|
|
486
|
+
return {
|
|
487
|
+
png: encodePng(canvas.width, canvas.height, canvas.data),
|
|
488
|
+
frameWidth: width,
|
|
489
|
+
frameHeight: height,
|
|
490
|
+
expressions: EXPRESSIONS.length,
|
|
491
|
+
};
|
|
492
|
+
}
|
|
493
|
+
|
|
494
|
+
// ------------------------------------------------------------------- SVG
|
|
495
|
+
|
|
496
|
+
/**
|
|
497
|
+
* A small vector icon, to verify SVG textures actually load through the compiled `data:`
|
|
498
|
+
* URI path (roadmap item 15) - not just resolve to one. `tools/compile-resources` already
|
|
499
|
+
* lists the MIME type; whether Pixi's SVG parser rasterises a `data:image/svg+xml` source
|
|
500
|
+
* correctly when it is reached through an *aliased* path with no `.svg` on the URL itself
|
|
501
|
+
* is the part that needed checking, not assumed from the MIME entry alone.
|
|
502
|
+
*/
|
|
503
|
+
function buildGemIcon() {
|
|
504
|
+
return (
|
|
505
|
+
'<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 32 32">' +
|
|
506
|
+
'<polygon points="16,2 30,12 25,30 7,30 2,12" fill="#d6b242" stroke="#8a6a1e" stroke-width="1.5"/>' +
|
|
507
|
+
'<polygon points="16,2 30,12 16,16" fill="#f0d97a"/>' +
|
|
508
|
+
'<polygon points="2,12 16,16 7,30" fill="#b8901e"/>' +
|
|
509
|
+
'</svg>\n'
|
|
510
|
+
);
|
|
511
|
+
}
|
|
512
|
+
|
|
513
|
+
// ---------------------------------------------------------------- sound
|
|
514
|
+
|
|
515
|
+
const SAMPLE_RATE = 22050;
|
|
516
|
+
|
|
517
|
+
function encodeWav(samples) {
|
|
518
|
+
const data = Buffer.alloc(samples.length * 2);
|
|
519
|
+
for (let i = 0; i < samples.length; i++) {
|
|
520
|
+
const clamped = Math.max(-1, Math.min(1, samples[i]));
|
|
521
|
+
data.writeInt16LE(Math.round(clamped * 0x7fff), i * 2);
|
|
522
|
+
}
|
|
523
|
+
|
|
524
|
+
const header = Buffer.alloc(44);
|
|
525
|
+
header.write('RIFF', 0, 'ascii');
|
|
526
|
+
header.writeUInt32LE(36 + data.length, 4);
|
|
527
|
+
header.write('WAVE', 8, 'ascii');
|
|
528
|
+
header.write('fmt ', 12, 'ascii');
|
|
529
|
+
header.writeUInt32LE(16, 16); //chunk size
|
|
530
|
+
header.writeUInt16LE(1, 20); //PCM
|
|
531
|
+
header.writeUInt16LE(1, 22); //mono
|
|
532
|
+
header.writeUInt32LE(SAMPLE_RATE, 24);
|
|
533
|
+
header.writeUInt32LE(SAMPLE_RATE * 2, 28); //byte rate
|
|
534
|
+
header.writeUInt16LE(2, 32); //block align
|
|
535
|
+
header.writeUInt16LE(16, 34); //bits per sample
|
|
536
|
+
header.write('data', 36, 'ascii');
|
|
537
|
+
header.writeUInt32LE(data.length, 40);
|
|
538
|
+
|
|
539
|
+
return Buffer.concat([header, data]);
|
|
540
|
+
}
|
|
541
|
+
|
|
542
|
+
/**
|
|
543
|
+
* @param duration seconds
|
|
544
|
+
* @param voice receives the time in seconds and the progress 0..1, returns -1..1
|
|
545
|
+
*/
|
|
546
|
+
function synth(duration, voice) {
|
|
547
|
+
const count = Math.floor(duration * SAMPLE_RATE);
|
|
548
|
+
const samples = new Float32Array(count);
|
|
549
|
+
for (let i = 0; i < count; i++) {
|
|
550
|
+
samples[i] = voice(i / SAMPLE_RATE, i / count);
|
|
551
|
+
}
|
|
552
|
+
return samples;
|
|
553
|
+
}
|
|
554
|
+
|
|
555
|
+
const decay = (progress, sharpness = 5) => Math.exp(-sharpness * progress);
|
|
556
|
+
const square = (phase) => (Math.sin(phase) >= 0 ? 1 : -1);
|
|
557
|
+
|
|
558
|
+
const SOUNDS = {
|
|
559
|
+
//a short blip: menus, picking things up
|
|
560
|
+
'blip.wav': synth(0.09, (t, p) => 0.35 * decay(p, 9) * square(2 * Math.PI * 880 * t)),
|
|
561
|
+
|
|
562
|
+
//a duller thud, pitched down over its length: a step, a door
|
|
563
|
+
'step.wav': synth(0.12, (t, p) => 0.3 * decay(p, 12) * Math.sin(2 * Math.PI * (220 - 90 * p) * t)),
|
|
564
|
+
|
|
565
|
+
//noise burst plus a low tone: a hit landing
|
|
566
|
+
'hit.wav': synth(0.16, (t, p) => {
|
|
567
|
+
const noiseValue = (noise(Math.floor(t * SAMPLE_RATE), 0, 11) - 0.5) * 2;
|
|
568
|
+
return decay(p, 14) * (0.35 * noiseValue + 0.25 * Math.sin(2 * Math.PI * 140 * t));
|
|
569
|
+
}),
|
|
570
|
+
|
|
571
|
+
//a rising two-note figure: something good happened
|
|
572
|
+
'pickup.wav': synth(0.22, (t, p) => {
|
|
573
|
+
const frequency = p < 0.5 ? 660 : 990;
|
|
574
|
+
return 0.28 * decay(p, 4) * square(2 * Math.PI * frequency * t);
|
|
575
|
+
}),
|
|
576
|
+
};
|
|
577
|
+
|
|
578
|
+
/**
|
|
579
|
+
* A short melody loop: `notesHz` played one after another over `duration`, each with its
|
|
580
|
+
* own soft attack/decay so consecutive notes do not click into each other. For
|
|
581
|
+
* `examples/audio`'s `Music.playTracks` demo - three of these, not one, is the whole point:
|
|
582
|
+
* a playlist needs more than one track to show a "several tracks" behaviour at all.
|
|
583
|
+
*/
|
|
584
|
+
function melody(duration, notesHz, waveform) {
|
|
585
|
+
const noteDuration = duration / notesHz.length;
|
|
586
|
+
return synth(duration, (t) => {
|
|
587
|
+
const index = Math.min(notesHz.length - 1, Math.floor(t / noteDuration));
|
|
588
|
+
const localP = (t - index * noteDuration) / noteDuration;
|
|
589
|
+
return 0.25 * Math.sin(Math.PI * localP) * waveform(2 * Math.PI * notesHz[index] * t);
|
|
590
|
+
});
|
|
591
|
+
}
|
|
592
|
+
|
|
593
|
+
const MUSIC = {
|
|
594
|
+
//C E G C, sine - a gentle, rising figure
|
|
595
|
+
'tune_dawn.wav': melody(4, [261.63, 329.63, 392.0, 523.25], Math.sin),
|
|
596
|
+
//G B D F, square - punchier, for an active scene
|
|
597
|
+
'tune_march.wav': melody(4, [196.0, 246.94, 293.66, 349.23], square),
|
|
598
|
+
//A C F C(low), sine - a descending figure, for a quieter moment
|
|
599
|
+
'tune_dusk.wav': melody(4, [220.0, 261.63, 174.61, 130.81], Math.sin),
|
|
600
|
+
};
|
|
601
|
+
|
|
602
|
+
// ---------------------------------------------------------------- write it out
|
|
603
|
+
|
|
604
|
+
/** every file `writePlaceholderAssets` writes, so it can refuse to overwrite one first */
|
|
605
|
+
const NAMES = [
|
|
606
|
+
'tiles.png',
|
|
607
|
+
'backdrop_room.png',
|
|
608
|
+
'char_alice.png',
|
|
609
|
+
'char_bob.png',
|
|
610
|
+
...Object.keys(SOUNDS),
|
|
611
|
+
...Object.keys(MUSIC),
|
|
612
|
+
'icon_gem.svg',
|
|
613
|
+
'tiles.json',
|
|
614
|
+
];
|
|
615
|
+
|
|
616
|
+
/**
|
|
617
|
+
* Writes the placeholder set into `out`: a 16px tileset and its `tiles.json`, a backdrop, two
|
|
618
|
+
* characters, sound effects, two looping tunes and an SVG icon, all drawn and synthesised here,
|
|
619
|
+
* so they carry no licence but the project's own (item 389). Refuses to replace a file that
|
|
620
|
+
* already exists unless `force`, since `out` may be a game's real asset folder.
|
|
621
|
+
*/
|
|
622
|
+
export async function writePlaceholderAssets(out, { force = false } = {}) {
|
|
623
|
+
if (!force) {
|
|
624
|
+
const existing = NAMES.filter((name) => existsSync(join(out, name)));
|
|
625
|
+
if (existing.length) throw new Error(`${out} already holds ${existing.join(', ')}; pass force to replace them`);
|
|
626
|
+
}
|
|
627
|
+
await mkdir(out, { recursive: true });
|
|
628
|
+
const png = buildTileset();
|
|
629
|
+
await writeFile(join(out, 'tiles.png'), png);
|
|
630
|
+
|
|
631
|
+
const backdrop = buildBackdrop({
|
|
632
|
+
wall: [72, 62, 78],
|
|
633
|
+
floor: [96, 78, 62],
|
|
634
|
+
floorLine: [78, 62, 48],
|
|
635
|
+
sky: [126, 168, 208],
|
|
636
|
+
frame: [52, 44, 56],
|
|
637
|
+
});
|
|
638
|
+
await writeFile(join(out, 'backdrop_room.png'), backdrop);
|
|
639
|
+
|
|
640
|
+
//two people, one silhouette: only the five colours differ
|
|
641
|
+
const characters = {
|
|
642
|
+
'char_alice.png': buildCharacter({
|
|
643
|
+
skin: [236, 198, 168],
|
|
644
|
+
eyes: [64, 128, 92],
|
|
645
|
+
hair: [168, 84, 52],
|
|
646
|
+
top: [188, 92, 108],
|
|
647
|
+
bottom: [58, 62, 96],
|
|
648
|
+
}),
|
|
649
|
+
'char_bob.png': buildCharacter({
|
|
650
|
+
skin: [166, 122, 92],
|
|
651
|
+
eyes: [78, 108, 168],
|
|
652
|
+
hair: [42, 38, 40],
|
|
653
|
+
top: [96, 148, 120],
|
|
654
|
+
bottom: [92, 78, 62],
|
|
655
|
+
}),
|
|
656
|
+
};
|
|
657
|
+
for (const [name, character] of Object.entries(characters)) {
|
|
658
|
+
await writeFile(join(out, name), character.png);
|
|
659
|
+
}
|
|
660
|
+
|
|
661
|
+
for (const [name, samples] of Object.entries(SOUNDS)) {
|
|
662
|
+
await writeFile(join(out, name), encodeWav(samples));
|
|
663
|
+
}
|
|
664
|
+
|
|
665
|
+
for (const [name, samples] of Object.entries(MUSIC)) {
|
|
666
|
+
await writeFile(join(out, name), encodeWav(samples));
|
|
667
|
+
}
|
|
668
|
+
|
|
669
|
+
const gemIcon = buildGemIcon();
|
|
670
|
+
await writeFile(join(out, 'icon_gem.svg'), gemIcon);
|
|
671
|
+
|
|
672
|
+
//the examples import this rather than hard-coding tile numbers and frame sizes
|
|
673
|
+
await writeFile(
|
|
674
|
+
join(out, 'tiles.json'),
|
|
675
|
+
JSON.stringify(
|
|
676
|
+
{
|
|
677
|
+
tileSize: TILE,
|
|
678
|
+
columns: COLUMNS,
|
|
679
|
+
rows: ROWS,
|
|
680
|
+
tiles: TILES,
|
|
681
|
+
character: {
|
|
682
|
+
frameWidth: CHARACTER_FRAME.width,
|
|
683
|
+
frameHeight: CHARACTER_FRAME.height,
|
|
684
|
+
expressions: Object.fromEntries(EXPRESSIONS.map((name, i) => [name, i])),
|
|
685
|
+
},
|
|
686
|
+
},
|
|
687
|
+
null,
|
|
688
|
+
'\t',
|
|
689
|
+
) + '\n',
|
|
690
|
+
'utf8',
|
|
691
|
+
);
|
|
692
|
+
|
|
693
|
+
const kb = (n) => (n / 1024).toFixed(1) + ' KB';
|
|
694
|
+
console.log(`tiles.png ${kb(png.length)} (${COLUMNS}x${ROWS} tiles of ${TILE}px)`);
|
|
695
|
+
console.log(`backdrop_room.png ${kb(backdrop.length)} (320x180)`);
|
|
696
|
+
for (const [name, character] of Object.entries(characters)) {
|
|
697
|
+
console.log(
|
|
698
|
+
`${name.padEnd(12)}${kb(character.png.length)} (${character.expressions} expressions of ${character.frameWidth}x${character.frameHeight})`,
|
|
699
|
+
);
|
|
700
|
+
}
|
|
701
|
+
for (const [name, samples] of Object.entries(SOUNDS)) {
|
|
702
|
+
console.log(`${name.padEnd(12)}${kb(samples.length * 2 + 44)}`);
|
|
703
|
+
}
|
|
704
|
+
for (const [name, samples] of Object.entries(MUSIC)) {
|
|
705
|
+
console.log(`${name.padEnd(14)}${kb(samples.length * 2 + 44)}`);
|
|
706
|
+
}
|
|
707
|
+
console.log(`icon_gem.svg${kb(gemIcon.length)}`);
|
|
708
|
+
console.log(`\nwritten to ${out} - generated, so redistributable under the project licence`);
|
|
709
|
+
}
|
|
710
|
+
|
|
711
|
+
/**
|
|
712
|
+
* `mwg-placeholder-assets <folder> [--force]`: the set above, for a game to prototype with before
|
|
713
|
+
* its own art exists. With no folder inside this repository, it regenerates `examples/assets`.
|
|
714
|
+
*/
|
|
715
|
+
//realpath, since an npm `bin` shim reaches this file through a symlink
|
|
716
|
+
if (process.argv[1] && realpathSync(process.argv[1]) === fileURLToPath(import.meta.url)) {
|
|
717
|
+
const args = process.argv.slice(2);
|
|
718
|
+
const folder = args.find((arg) => !arg.startsWith('--'));
|
|
719
|
+
const inRepository = existsSync(join(here, '..', 'examples'));
|
|
720
|
+
if (!folder && !inRepository) {
|
|
721
|
+
console.error('usage: mwg-placeholder-assets <folder> [--force]');
|
|
722
|
+
process.exit(1);
|
|
723
|
+
}
|
|
724
|
+
await writePlaceholderAssets(folder ? resolve(folder) : EXAMPLE_ASSETS, {
|
|
725
|
+
force: !folder || args.includes('--force'),
|
|
726
|
+
});
|
|
727
|
+
}
|