@siegesailor/terminal-sigma 1.1.0 → 1.3.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.
@@ -1,386 +1,593 @@
1
- // The character is a 16x20 pixel canvas. Each terminal cell draws 2 stacked
2
- // pixels with "▀", foreground for the top and background for the bottom.
3
- export const spriteWidth = 16;
4
- export const spriteHeight = 20;
5
- const rows = (count, row) => Array.from({ length: count }, () => row);
6
- const head = {
7
- y: 2,
8
- rows: [
9
- "....kkkkkkkk....",
10
- "...khhhhhhhhk...",
11
- "..khhhhhhhhhhk..",
12
- "..khsssssssshk..",
13
- "..kssessssessk..",
14
- "..ksrssmmssrsk..",
15
- "...kssssssssk...",
16
- "....kkksskkk....",
17
- ],
18
- };
19
- const body = {
20
- y: 10,
21
- rows: [
22
- "...kaaaaaaaak...",
23
- "...kAaaaaaaAk...",
24
- "...kAaaaaaaAk...",
25
- "...knnnnnnnnk...",
26
- "...kppppppppk...",
27
- ],
28
- };
29
- const straps = { y: 10, rows: rows(3, "......P..P......") };
30
- const leftArm = {
31
- y: 10,
32
- rows: ["..kA............", "..kA............", "..ks............"],
33
- };
34
- const rightArm = {
35
- y: 10,
36
- rows: ["............Ak..", "............Ak..", "............sk.."],
37
- };
38
- const swingingArms = {
39
- y: 10,
40
- rows: ["..kA........Ak..", ".ks..........sk."],
41
- };
42
- const raisedArms = {
43
- y: 3,
44
- rows: [
45
- ".s............s.",
46
- ...rows(4, ".A............A."),
47
- ...rows(3, "..A..........A.."),
48
- ],
1
+ // The character stands on a stage as wide as its panel and 30 pixels tall.
2
+ // Each terminal cell draws 2 stacked pixels with "▀", foreground for the top
3
+ // and background for the bottom, so the stage takes 15 rows.
4
+ export const stageHeight = 30;
5
+ export const defaultBody = {
6
+ height: 23,
7
+ build: "average",
8
+ gender: "other",
49
9
  };
50
- const standingLegs = {
51
- y: 15,
52
- rows: [
53
- "....kpp..ppk....",
54
- "....kpp..ppk....",
55
- "....kbb..bbk....",
56
- "...kbbb..bbbk...",
57
- ],
58
- };
59
- const stridingLegs = {
60
- y: 15,
61
- rows: ["....kpp..ppk....", "...kpp....ppk...", "..kbb......bbk.."],
62
- };
63
- const passingLegs = {
64
- y: 15,
65
- rows: [
66
- ".....kppppk.....",
67
- "......kppk......",
68
- "......kbbk......",
69
- ".....kbbbbk.....",
70
- ],
10
+ // 145 cm draws 20 pixels tall and 205 cm 26; the build follows the BMI.
11
+ export function bodyOf(profile) {
12
+ if (!profile) {
13
+ return defaultBody;
14
+ }
15
+ const bmi = profile.weight / (profile.height / 100) ** 2;
16
+ let build = "average";
17
+ if (bmi < 20) {
18
+ build = "slim";
19
+ }
20
+ else if (bmi >= 27) {
21
+ build = "broad";
22
+ }
23
+ return {
24
+ height: Math.min(Math.max(Math.round(20 + (profile.height - 145) / 10), 20), 26),
25
+ build,
26
+ gender: profile.gender,
27
+ };
28
+ }
29
+ // Each equipment slot upgrades once from level 2 and again 4 levels later,
30
+ // staggered so every level from 2 to 9 changes exactly 1 piece.
31
+ export const gearOf = (level) => {
32
+ const tier = (slot) => Math.min(Math.max(Math.floor((level - 2 - slot) / 4) + 1, 0), 2);
33
+ return { gadget: tier(0), top: tier(1), headwear: tier(2), back: tier(3) };
71
34
  };
72
- const sweat = { y: 3, rows: ["..............d."] };
73
- const motionLines = {
74
- y: 15,
75
- rows: ["vv..............", "................", "v..............."],
35
+ const widthOf = { slim: 4, average: 5, broad: 6 };
36
+ const dimsOf = (body) => {
37
+ // Shoes, neck, a 4-row head, and its top row take 7 of the height.
38
+ const span = body.height - 7;
39
+ const torso = Math.round(span * 0.4);
40
+ const legs = span - torso;
41
+ const width = widthOf[body.build];
42
+ const legWidth = body.build === "broad" ? 3 : 2;
43
+ const right = Math.floor(width / 2) - 1;
44
+ return {
45
+ legWidth,
46
+ frontHip: right - legWidth + 1,
47
+ backHip: -Math.ceil(width / 2),
48
+ legs,
49
+ torso,
50
+ left: -Math.ceil(width / 2),
51
+ right: Math.floor(width / 2) - 1,
52
+ shoulder: legs + torso,
53
+ neck: legs + torso + 1,
54
+ arm: torso - 1,
55
+ };
76
56
  };
77
- const laptop = {
78
- y: 12,
79
- rows: [
80
- "..kggggggggggk..",
81
- "..kggggLLggggk..",
82
- "..kggggLLggggk..",
83
- "..kggggggggggk..",
84
- ".kkkkkkkkkkkkkk.",
85
- ],
57
+ const pointsOf = ([x0, y0], [x1, y1]) => {
58
+ const points = [];
59
+ const steps = Math.max(Math.abs(x1 - x0), Math.abs(y1 - y0), 1);
60
+ for (let step = 0; step <= steps; step++) {
61
+ points.push([
62
+ Math.round(x0 + ((x1 - x0) * step) / steps),
63
+ Math.round(y0 + ((y1 - y0) * step) / steps),
64
+ ]);
65
+ }
66
+ return points;
86
67
  };
87
- const typingArms = (hands) => ({
88
- y: 10,
89
- rows: ["..kA........Ak..", hands],
68
+ // Feet are relative to their own hip.
69
+ const standing = (d, hand = [0, -d.arm]) => ({
70
+ front: [0, 0],
71
+ back: [0, 0],
72
+ frontHand: hand,
73
+ backHand: [-1, -d.arm],
90
74
  });
91
- const cup = {
92
- y: 11,
93
- rows: [".............ccc", ".............chc", ".............ccc"],
94
- };
95
- const steam = (y, row) => ({ y, rows: [row] });
96
- const appleAtMouth = {
97
- y: 6,
98
- rows: [
99
- ".........q......",
100
- ".........ff.....",
101
- ".........ff.....",
102
- "..........s.....",
103
- "..........AAk...",
75
+ const walkFeet = [
76
+ [
77
+ [2, 0],
78
+ [-1, 0],
104
79
  ],
105
- };
106
- const appleInHand = {
107
- y: 11,
108
- rows: ["...............q", "..............ff", "..............ff"],
109
- };
110
- const chewing = { y: 7, rows: [".......oo......."] };
111
- const dumbbellsUp = {
112
- y: 1,
113
- rows: [
114
- "GG............GG",
115
- "GG............GG",
116
- ".s............s.",
117
- ...rows(6, ".A............A."),
118
- "..A..........A..",
80
+ [
81
+ [0, 0],
82
+ [0, 1],
119
83
  ],
120
- };
121
- const dumbbellsDown = {
122
- y: 7,
123
- rows: [
124
- "GG............GG",
125
- "GG............GG",
126
- ".s............s.",
127
- ".AA..........AA.",
84
+ [
85
+ [-3, 0],
86
+ [4, 0],
87
+ ],
88
+ [
89
+ [-2, 1],
90
+ [2, 0],
128
91
  ],
129
- };
130
- const sparkles = (top, y, bottom) => [
131
- { y: 0, rows: [top] },
132
- { y, rows: [bottom] },
133
- ];
134
- const weapons = [
135
- { y: 2, rows: ["..............O.", ...rows(15, "..............W.")] },
136
- { y: 4, rows: [".O..............", ...rows(11, ".G..............")] },
137
- {
138
- y: 0,
139
- rows: [
140
- ...rows(11, "..............B."),
141
- ".............yyy",
142
- "..............n.",
143
- "..............n.",
144
- ],
145
- },
146
92
  ];
147
- const helmets = [
93
+ const runFeet = [
148
94
  [
149
- {
150
- y: 2,
151
- rows: [
152
- "....HHHHHHHH....",
153
- "...HHHHHHHHHH...",
154
- "..HHH......HHH..",
155
- ...rows(3, "..HH........HH.."),
156
- "...H........H...",
157
- ],
158
- },
95
+ [3, 1],
96
+ [-2, 2],
159
97
  ],
160
98
  [
161
- {
162
- y: 1,
163
- rows: ["...........V....", "....MMMMMMMM....", "...kMMMMMMMMk..."],
164
- },
165
- { y: 6, rows: ["..kVVVVVVVVVVk.."] },
99
+ [0, 0],
100
+ [0, 3],
166
101
  ],
167
102
  [
168
- {
169
- y: 0,
170
- rows: [
171
- ".Y............Y.",
172
- ".YY..........YY.",
173
- "..YYMMMMMMMMYY..",
174
- "...kMMMMMMMMk...",
175
- "..kMMMMMMMMMMk..",
176
- ],
177
- },
103
+ [-4, 2],
104
+ [5, 1],
105
+ ],
106
+ [
107
+ [-2, 3],
108
+ [2, 0],
178
109
  ],
179
110
  ];
180
- const arms = [leftArm, rightArm];
181
- const animations = {
182
- idle: {
183
- interval: 300,
184
- frames: [
185
- ...Array.from({ length: 11 }, () => ({
186
- layers: [...arms, standingLegs],
187
- })),
188
- { layers: [...arms, standingLegs], blink: true },
189
- ],
190
- },
191
- focus: {
192
- interval: 250,
193
- frames: [
194
- {
195
- layers: [
196
- typingArms("...ks......sk..."),
197
- standingLegs,
198
- laptop,
199
- steam(1, "............O..."),
111
+ const stride = (d, feet, step, swing, lift = 0) => {
112
+ const [front, back] = feet[step % feet.length] ?? [
113
+ [0, 0],
114
+ [0, 0],
115
+ ];
116
+ const sway = step % 2 === 0 ? swing : 0;
117
+ const direction = step % 4 < 2 ? -1 : 1;
118
+ return {
119
+ front,
120
+ back,
121
+ frontHand: [direction * sway, -d.arm + (sway ? 1 : 0)],
122
+ backHand: [-direction * sway, -d.arm + (sway ? 1 : 0)],
123
+ lift: step % 2 === 1 ? lift : 0,
124
+ };
125
+ };
126
+ const sparkle = (points) => (put) => {
127
+ for (const [x, y] of points) {
128
+ put(x, y, "y");
129
+ }
130
+ };
131
+ const desk = (put, d) => {
132
+ const top = d.legs + 1;
133
+ for (let x = 3; x <= 9; x++) {
134
+ put(x, top, "x");
135
+ }
136
+ for (let y = 0; y < top; y++) {
137
+ put(8, y, "x");
138
+ }
139
+ for (let x = 4; x <= 7; x++) {
140
+ put(x, top + 1, "L");
141
+ }
142
+ for (let y = top + 2; y <= top + 5; y++) {
143
+ put(6, y, "l");
144
+ put(7, y, "L");
145
+ }
146
+ };
147
+ const cup = (at, steamStep) => (put, d) => {
148
+ const [x, y] = at(d);
149
+ put(x, y, "Q");
150
+ put(x + 1, y, "Q");
151
+ put(x, y + 1, "o");
152
+ put(x + 1, y + 1, "Q");
153
+ if (steamStep !== undefined) {
154
+ const sway = steamStep % 2;
155
+ put(x + sway, y + 3, "v");
156
+ put(x + 1 - sway, y + 4, "v");
157
+ }
158
+ };
159
+ const apple = (at, bitten = false) => (put, d) => {
160
+ const [x, y] = at(d);
161
+ put(x, y, "F");
162
+ put(x + 1, y, bitten ? "." : "F");
163
+ put(x, y + 1, "F");
164
+ put(x + 1, y + 1, "F");
165
+ put(x + 1, y + 2, "G");
166
+ };
167
+ const dumbbell = (at) => (put, d) => {
168
+ const [x, y] = at(d);
169
+ put(x, y - 1, "D");
170
+ put(x, y, "d");
171
+ put(x, y + 1, "D");
172
+ };
173
+ const shoulderOf = (d) => [d.right, d.shoulder];
174
+ const handAt = (d, [dx, dy]) => [
175
+ d.right + dx,
176
+ d.shoulder + dy,
177
+ ];
178
+ // Where the character stands and what it does on every tick of `counter`.
179
+ function sceneOf(activity, counter, width) {
180
+ const home = 6;
181
+ const minimum = 5;
182
+ const maximum = Math.max(width - 7, minimum + 1);
183
+ const span = maximum - minimum;
184
+ switch (activity) {
185
+ case "idle": {
186
+ // Wander: pause, walk across, pause and look around, walk back.
187
+ const pause = 8;
188
+ const cycle = 2 * (pause + span);
189
+ const tick = counter % cycle;
190
+ const breathe = (d, at) => ({
191
+ ...standing(d),
192
+ bob: Math.floor(at / 2) % 2,
193
+ blink: at % pause === 5,
194
+ });
195
+ if (tick < pause) {
196
+ return {
197
+ x: minimum,
198
+ facing: tick === 3 || tick === 4 ? -1 : 1,
199
+ pose: (d) => breathe(d, tick),
200
+ };
201
+ }
202
+ if (tick < pause + span) {
203
+ const step = tick - pause;
204
+ return {
205
+ x: minimum + step,
206
+ facing: 1,
207
+ pose: (d) => stride(d, walkFeet, step, 1),
208
+ };
209
+ }
210
+ if (tick < 2 * pause + span) {
211
+ const at = tick - pause - span;
212
+ return {
213
+ x: maximum,
214
+ facing: at === 3 || at === 4 ? 1 : -1,
215
+ pose: (d) => breathe(d, at),
216
+ };
217
+ }
218
+ const step = tick - 2 * pause - span;
219
+ return {
220
+ x: maximum - step,
221
+ facing: -1,
222
+ pose: (d) => stride(d, walkFeet, step, 1),
223
+ };
224
+ }
225
+ case "run": {
226
+ // Laps across the panel, 2 pixels a tick.
227
+ const lap = Math.max(Math.ceil(span / 2), 1);
228
+ const tick = counter % (2 * lap);
229
+ const forward = tick < lap;
230
+ const step = forward ? tick : tick - lap;
231
+ return {
232
+ x: forward
233
+ ? Math.min(minimum + step * 2, maximum)
234
+ : Math.max(maximum - step * 2, minimum),
235
+ facing: forward ? 1 : -1,
236
+ pose: (d) => stride(d, runFeet, counter, 2, 1),
237
+ props: [
238
+ sparkle(counter % 2 === 0
239
+ ? [
240
+ [-6, 6],
241
+ [-7, 9],
242
+ ]
243
+ : [
244
+ [-7, 7],
245
+ [-6, 10],
246
+ ]),
200
247
  ],
201
- },
202
- {
203
- layers: [
204
- typingArms("...kss....ssk..."),
205
- standingLegs,
206
- laptop,
207
- steam(0, "..............O."),
248
+ };
249
+ }
250
+ case "focus": {
251
+ const step = counter % 4;
252
+ return {
253
+ x: home,
254
+ facing: 1,
255
+ pose: (d) => ({
256
+ ...standing(d),
257
+ frontHand: [4 - d.right + (step % 2), d.legs + 2 - d.shoulder],
258
+ backHand: [3 - d.right, d.legs + 2 - d.shoulder],
259
+ bob: step < 2 ? 0 : 1,
260
+ }),
261
+ props: [
262
+ desk,
263
+ (put, d) => {
264
+ const top = d.legs + 7;
265
+ put(6 + (step % 3), top + step, "y");
266
+ put(8 - (step % 2), top + ((step + 2) % 4), "y");
267
+ },
208
268
  ],
209
- },
210
- ],
211
- },
212
- break: {
213
- interval: 600,
214
- frames: [
215
- {
216
- layers: [...arms, standingLegs, cup, steam(10, "..............v.")],
217
- },
218
- {
219
- layers: [
220
- ...arms,
221
- standingLegs,
222
- cup,
223
- steam(9, "..............v."),
224
- steam(8, ".............v.."),
269
+ };
270
+ }
271
+ case "break": {
272
+ const step = counter % 6;
273
+ const sipping = step >= 4;
274
+ const hand = (d) => sipping ? [2, d.neck + 1 - d.shoulder] : [2, -d.arm + 2];
275
+ return {
276
+ x: home,
277
+ facing: 1,
278
+ pose: (d) => ({
279
+ ...standing(d, hand(d)),
280
+ bob: step % 2,
281
+ blink: sipping,
282
+ }),
283
+ props: [
284
+ cup((d) => {
285
+ const [x, y] = handAt(d, hand(d));
286
+ return [x + 1, y];
287
+ }, sipping ? undefined : step),
225
288
  ],
226
- blink: true,
227
- },
228
- ],
229
- },
230
- eat: {
231
- interval: 400,
232
- frames: [
233
- { layers: [leftArm, standingLegs, appleAtMouth] },
234
- { layers: [...arms, standingLegs, appleInHand, chewing] },
235
- ],
236
- },
237
- run: {
238
- interval: 180,
239
- frames: [
240
- { layers: [swingingArms, stridingLegs, sweat, motionLines] },
241
- { layers: [...arms, passingLegs], dy: -1 },
242
- ],
243
- },
244
- lift: {
245
- interval: 500,
246
- frames: [
247
- { layers: [dumbbellsUp, standingLegs, sweat] },
248
- { layers: [dumbbellsDown, standingLegs] },
249
- ],
250
- },
251
- stretch: {
252
- interval: 700,
253
- frames: [
254
- { layers: [raisedArms, standingLegs] },
255
- { layers: [...arms, standingLegs] },
256
- ],
257
- },
258
- levelUp: {
259
- interval: 250,
260
- frames: [
261
- {
262
- layers: [
263
- raisedArms,
264
- standingLegs,
265
- ...sparkles("...y........y...", 13, "y..............y"),
289
+ };
290
+ }
291
+ case "eat": {
292
+ const step = counter % 4;
293
+ const atMouth = step === 1 || step === 2;
294
+ const hand = (d) => atMouth ? [2, d.neck + 1 - d.shoulder] : [2, -d.arm + 2];
295
+ return {
296
+ x: home,
297
+ facing: 1,
298
+ pose: (d) => ({ ...standing(d, hand(d)), bob: step === 2 ? 1 : 0 }),
299
+ props: [
300
+ apple((d) => {
301
+ const [x, y] = handAt(d, hand(d));
302
+ return [x + 1, y];
303
+ }, step === 3),
266
304
  ],
267
- },
268
- {
269
- layers: [
270
- raisedArms,
271
- standingLegs,
272
- ...sparkles("y..............y", 16, "..y..........y.."),
305
+ };
306
+ }
307
+ case "lift": {
308
+ const step = counter % 4;
309
+ const hand = (d) => step === 2 ? [1, d.arm] : [2, step === 0 ? -d.arm + 3 : 1];
310
+ return {
311
+ x: home,
312
+ facing: 1,
313
+ pose: (d) => ({
314
+ ...standing(d, hand(d)),
315
+ backHand: [-1, -d.arm],
316
+ bob: step % 2,
317
+ }),
318
+ props: [dumbbell((d) => handAt(d, hand(d)))],
319
+ };
320
+ }
321
+ case "stretch": {
322
+ const step = counter % 4;
323
+ const up = step < 2;
324
+ return {
325
+ x: home,
326
+ facing: 1,
327
+ pose: (d) => ({
328
+ ...standing(d, up ? [step, d.arm] : [d.arm, step - 2]),
329
+ backHand: up ? [-step, d.arm] : [d.arm - 1, step - 3],
330
+ bob: step % 2,
331
+ }),
332
+ };
333
+ }
334
+ case "levelUp": {
335
+ const step = counter % 4;
336
+ const lift = [0, 2, 3, 1][step] ?? 0;
337
+ return {
338
+ x: home,
339
+ facing: 1,
340
+ pose: (d) => ({ ...standing(d, [1, d.arm]), lift }),
341
+ props: [
342
+ sparkle(step % 2 === 0
343
+ ? [
344
+ [-5, 20],
345
+ [5, 22],
346
+ [-6, 12],
347
+ [6, 14],
348
+ ]
349
+ : [
350
+ [-6, 22],
351
+ [6, 20],
352
+ [-5, 14],
353
+ [5, 12],
354
+ ]),
273
355
  ],
274
- dy: -1,
275
- },
276
- ],
277
- },
356
+ };
357
+ }
358
+ }
359
+ }
360
+ export const intervalOf = (activity) => ({
361
+ idle: 200,
362
+ focus: 180,
363
+ break: 380,
364
+ eat: 300,
365
+ run: 110,
366
+ lift: 400,
367
+ stretch: 480,
368
+ levelUp: 160,
369
+ })[activity];
370
+ // How many ticks one loop of an activity takes on a stage of `width`.
371
+ export function cycleOf(activity, width) {
372
+ const walk = Math.max(width - 7, 6) - 5;
373
+ if (activity === "idle") {
374
+ return 2 * (8 + walk);
375
+ }
376
+ if (activity === "run") {
377
+ return 2 * Math.max(Math.ceil(walk / 2), 1);
378
+ }
379
+ return activity === "break" ? 6 : 4;
380
+ }
381
+ const span = (put, from, to, y, key) => {
382
+ for (let x = from; x <= to; x++) {
383
+ put(x, y, key);
384
+ }
278
385
  };
279
- export const animationOf = (activity) => animations[activity];
280
- // Each gear slot upgrades once from level 2 and again 4 levels later, staggered
281
- // so every level from 2 to 9 changes exactly 1 piece.
282
- export const gearOf = (level) => {
283
- const tier = (slot) => Math.min(Math.max(Math.floor((level - 2 - slot) / 4) + 1, 0), 2);
284
- return {
285
- weapon: tier(0),
286
- armor: tier(1),
287
- helmet: tier(2),
288
- backpack: tier(3),
386
+ function paintArm({ upper, gear }, shoulder, hand, isFront) {
387
+ const points = pointsOf(shoulder, [
388
+ shoulder[0] + hand[0],
389
+ shoulder[1] + hand[1],
390
+ ]);
391
+ const sleeve = gear.top === 0 ? "U" : "c";
392
+ const skin = isFront ? "s" : "q";
393
+ for (const [index, [x, y]] of points.entries()) {
394
+ const isHand = index === points.length - 1;
395
+ const isBare = gear.top === 0 && index >= 2;
396
+ upper(x, y, isHand || isBare ? skin : sleeve);
397
+ }
398
+ const wrist = points.at(-2);
399
+ if (isFront && gear.gadget === 2 && wrist && points.length > 2) {
400
+ upper(wrist[0], wrist[1], "t");
401
+ }
402
+ }
403
+ // Behind the body: a cape that flares and ripples, or a backpack.
404
+ function paintBack({ upper, d, gear, counter }) {
405
+ if (gear.back === 2) {
406
+ for (let row = 0; row <= d.torso + 3; row++) {
407
+ const flare = Math.floor(row / 3) + ((counter + row) % 4 < 2 ? 0 : 1);
408
+ upper(d.left - 1 - flare, d.shoulder - row, "z");
409
+ span(upper, d.left - flare, d.left, d.shoulder - row, "S");
410
+ }
411
+ }
412
+ else if (gear.back === 1) {
413
+ for (let row = 1; row <= 5; row++) {
414
+ upper(d.left - 1, d.shoulder - row, row === 1 ? "B" : "b");
415
+ upper(d.left - 2, d.shoulder - row, "b");
416
+ }
417
+ }
418
+ }
419
+ function paintLegs({ put, d, pose }) {
420
+ const leg = (hip, foot, pants, shoe) => {
421
+ const footX = hip + foot[0];
422
+ for (const [x, y] of pointsOf([hip, d.legs], [footX, foot[1] + 1])) {
423
+ span(put, x, x + d.legWidth - 1, y, pants);
424
+ }
425
+ span(put, footX, footX + d.legWidth, foot[1], shoe);
289
426
  };
290
- };
291
- const fixedColors = [
292
- ["b", "#3a2a24"],
293
- ["B", "#cfe8ff"],
294
- ["c", "#f4f4f4"],
295
- ["d", "#7fd3ff"],
296
- ["e", "#2b2135"],
297
- ["f", "#e5484d"],
298
- ["g", "#9aa3ad"],
299
- ["G", "#6e7681"],
300
- ["h", "#6b4226"],
301
- ["H", "#8e5cc4"],
302
- ["k", "#2b2135"],
303
- ["L", "#D97757"],
304
- ["m", "#b5524f"],
305
- ["M", "#b8c0cc"],
306
- ["n", "#7a4f2a"],
307
- ["o", "#7a2e2e"],
308
- ["O", "#57e3f0"],
309
- ["p", "#3d4f7a"],
310
- ["q", "#4caf6a"],
311
- ["r", "#f08c8c"],
312
- ["s", "#f5c9a0"],
313
- ["v", "#c8c8c8"],
314
- ["V", "#57e3f0"],
315
- ["W", "#9b6b3d"],
316
- ["y", "#ffd84d"],
317
- ["Y", "#f2e6c8"],
318
- ];
319
- const armorColors = [
320
- ["#4caf6a", "#2f7a45"],
321
- ["#d9a440", "#9c7322"],
322
- ["#4fb3e3", "#2a7fb0"],
323
- ];
324
- const backpackColors = ["#8a6d3b", "#c08a2e", "#57e3f0"];
325
- export function paletteOf(level) {
326
- const gear = gearOf(level);
327
- const [armor, armorShade] = armorColors[gear.armor] ?? armorColors[0];
427
+ leg(d.backHip, pose.back, "P", "W");
428
+ leg(d.frontHip, pose.front, "p", "w");
429
+ }
430
+ // Men carry width at the shoulders, women at the hips.
431
+ function paintTorso({ upper, d, body, gear }) {
432
+ const [coat, shade] = gear.top === 0 ? ["T", "U"] : ["C", "c"];
433
+ for (let row = 0; row < d.torso; row++) {
434
+ const narrowed = (body.gender === "female" && row >= d.torso - 2) ||
435
+ (body.gender === "male" && row < 2);
436
+ const left = d.left + (narrowed ? 1 : 0);
437
+ upper(left, d.legs + 1 + row, shade);
438
+ span(upper, left + 1, d.right, d.legs + 1 + row, coat);
439
+ }
440
+ if (gear.top === 1) {
441
+ span(upper, d.left, d.left + 1, d.shoulder + 1, shade);
442
+ }
443
+ else if (gear.top === 2) {
444
+ for (let row = 0; row < d.torso; row++) {
445
+ upper(d.right, d.legs + 1 + row, "S");
446
+ }
447
+ upper(d.right - 1, d.shoulder, "S");
448
+ }
449
+ }
450
+ // Facing forward, with the hair at the back of the head.
451
+ function paintHead({ upper, d, body, pose }) {
452
+ const n = d.neck;
453
+ span(upper, -1, 0, n, "q");
454
+ for (let row = 1; row <= 4; row++) {
455
+ span(upper, -1, 1, n + row, "s");
456
+ upper(-2, n + row, "h");
457
+ }
458
+ upper(2, n + 2, "s");
459
+ upper(1, n + 3, pose.blink ? "s" : "e");
460
+ span(upper, -2, 1, n + 5, "h");
461
+ upper(-1, n + 4, "h");
462
+ if (body.gender === "female") {
463
+ for (let y = n - 2; y <= n + 4; y++) {
464
+ upper(-3, y, "h");
465
+ }
466
+ span(upper, -2, -2, n - 1, "h");
467
+ upper(-2, n, "h");
468
+ upper(0, n + 4, "h");
469
+ }
470
+ else if (body.gender === "other") {
471
+ for (let y = n + 1; y <= n + 3; y++) {
472
+ upper(-3, y, "h");
473
+ }
474
+ }
475
+ }
476
+ // Headphones go on first, so a cap or a crown sits over their band.
477
+ function paintHeadgear({ upper, d, gear }) {
478
+ const n = d.neck;
479
+ if (gear.gadget === 1) {
480
+ upper(-2, n, "S");
481
+ upper(1, n, "S");
482
+ }
483
+ else if (gear.gadget === 2) {
484
+ span(upper, -2, 1, n + 5, "k");
485
+ upper(-1, n + 2, "S");
486
+ upper(-1, n + 3, "S");
487
+ }
488
+ if (gear.headwear === 1) {
489
+ span(upper, -2, 1, n + 5, "S");
490
+ span(upper, -2, 0, n + 6, "S");
491
+ span(upper, 2, 3, n + 5, "z");
492
+ }
493
+ else if (gear.headwear === 2) {
494
+ span(upper, -2, 1, n + 6, "g");
495
+ upper(-2, n + 7, "g");
496
+ upper(0, n + 7, "g");
497
+ upper(-1, n + 6, "t");
498
+ }
499
+ }
500
+ function drawCharacter(paint) {
501
+ const { d, pose } = paint;
502
+ paintBack(paint);
503
+ paintArm(paint, [d.left + 1, d.shoulder], pose.backHand, false);
504
+ paintLegs(paint);
505
+ paintTorso(paint);
506
+ paintHead(paint);
507
+ paintHeadgear(paint);
508
+ paintArm(paint, shoulderOf(d), pose.frontHand, true);
509
+ }
510
+ export function paletteOf(palette) {
511
+ const { cloak, cloakShade, scarf, scarfShade, glow } = palette.sprite;
328
512
  return new Map([
329
- ...fixedColors,
330
- ["a", armor],
331
- ["A", armorShade],
332
- ["P", backpackColors[gear.backpack] ?? backpackColors[0]],
513
+ ["B", "#3E3226"],
514
+ ["b", "#5A4632"],
515
+ ["C", cloak],
516
+ ["c", cloakShade],
517
+ ["D", "#50555F"],
518
+ ["d", "#8A8F99"],
519
+ ["e", "#2B2B2B"],
520
+ ["F", "#C8473F"],
521
+ ["G", "#5F8F4E"],
522
+ ["g", "#D9B45A"],
523
+ ["h", "#6B4A2E"],
524
+ ["k", "#2B2B2B"],
525
+ ["L", "#4A505B"],
526
+ ["l", glow],
527
+ ["o", "#5A3A25"],
528
+ ["P", "#3E6870"],
529
+ ["p", "#4F8189"],
530
+ ["Q", "#E6E0D4"],
531
+ ["q", "#C99872"],
532
+ ["S", scarf],
533
+ ["s", "#E2B48C"],
534
+ ["T", "#DCD7CC"],
535
+ ["t", glow],
536
+ ["U", "#B4AE9F"],
537
+ ["v", "#9AA0A8"],
538
+ ["W", "#C8C8C8"],
539
+ ["w", "#F2F2F2"],
540
+ ["x", "#3D414B"],
541
+ ["y", palette.soft],
542
+ ["z", scarfShade],
333
543
  ]);
334
544
  }
335
545
  // The pixel keys of 1 frame, row by row, "." where nothing is drawn.
336
- export function composeFrame(activity, frameIndex, level) {
337
- const { frames } = animations[activity];
338
- const frame = frames[frameIndex % frames.length] ?? { layers: [] };
339
- const gear = gearOf(level);
340
- const canvas = rows(spriteHeight, ".".repeat(spriteWidth)).map((row) => [
341
- ...row,
342
- ]);
343
- const faceLayer = frame.blink
344
- ? { y: head.y, rows: head.rows.map((row) => row.replaceAll("e", "s")) }
345
- : head;
346
- const layers = [
347
- weapons[gear.weapon],
348
- faceLayer,
349
- body,
350
- straps,
351
- ...(helmets[gear.helmet] ?? []),
352
- ...frame.layers,
353
- ];
354
- for (const layer of layers) {
355
- if (!layer) {
356
- continue;
357
- }
358
- for (const [offset, row] of layer.rows.entries()) {
359
- const target = canvas[layer.y + offset + (frame.dy ?? 0)];
360
- for (const [x, key] of [...row].entries()) {
361
- if (target && key !== ".") {
362
- target[x] = key;
363
- }
364
- }
546
+ export function composeScene({ activity, counter, level, body, width, }) {
547
+ const canvas = Array.from({ length: stageHeight }, () => Array.from({ length: width }, () => "."));
548
+ const scene = sceneOf(activity, counter, width);
549
+ const d = dimsOf(body);
550
+ const pose = scene.pose(d);
551
+ const lift = pose.lift ?? 0;
552
+ const put = (x, y, key) => {
553
+ const column = scene.x + scene.facing * x;
554
+ const row = canvas[stageHeight - 1 - (y + lift)];
555
+ if (row && column >= 0 && column < width && key !== ".") {
556
+ row[column] = key;
365
557
  }
558
+ };
559
+ drawCharacter({
560
+ put,
561
+ upper(x, y, key) {
562
+ put(x, y - (pose.bob ?? 0), key);
563
+ },
564
+ d,
565
+ body,
566
+ gear: gearOf(level),
567
+ pose,
568
+ counter,
569
+ });
570
+ for (const draw of scene.props ?? []) {
571
+ draw(put, d);
366
572
  }
367
573
  return canvas.map((row) => row.join(""));
368
574
  }
369
575
  // Pairs of pixel rows become 1 row of "▀"/"▄" cells, merged into styled runs.
370
576
  export function toSegments(frame, palette) {
371
577
  const lines = [];
578
+ const width = frame[0]?.length ?? 0;
372
579
  for (let y = 0; y < frame.length; y += 2) {
373
580
  const top = frame[y] ?? "";
374
581
  const bottom = frame[y + 1] ?? "";
375
582
  const line = [];
376
- for (let x = 0; x < spriteWidth; x++) {
377
- const upper = palette.get(top[x] ?? ".");
378
- const lower = palette.get(bottom[x] ?? ".");
379
- const cell = upper === undefined
380
- ? lower === undefined
583
+ for (let x = 0; x < width; x++) {
584
+ const upperColor = palette.get(top[x] ?? ".");
585
+ const lowerColor = palette.get(bottom[x] ?? ".");
586
+ const cell = upperColor === undefined
587
+ ? lowerColor === undefined
381
588
  ? { text: " " }
382
- : { text: "▄", color: lower }
383
- : { text: "▀", color: upper, backgroundColor: lower };
589
+ : { text: "▄", color: lowerColor }
590
+ : { text: "▀", color: upperColor, backgroundColor: lowerColor };
384
591
  const previous = line.at(-1);
385
592
  if (previous &&
386
593
  previous.color === cell.color &&