@siegesailor/terminal-sigma 1.2.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,336 +1,586 @@
1
- // The character is a 20x28 pixel canvas, facing right. Each terminal cell draws
2
- // 2 stacked pixels with "▀", foreground for the top and background for the bottom.
3
- export const spriteWidth = 20;
4
- export const spriteHeight = 28;
5
- const layer = (x, y, ...rows) => ({
6
- x,
7
- y,
8
- rows,
9
- });
10
- const upper = (x, y, ...rows) => ({
11
- x,
12
- y,
13
- rows,
14
- upper: true,
15
- });
16
- const hood = upper(0, 2, "......kkkk", "....kkHHHHkk", "..kkHHHHHHHhk", "kkHHHHHHHHhffk", "..kHHHHHHhffEfk", "...kHHHHHhfffk", "....khhHHhffk", "....kkkkkkkk");
17
- const neckScarf = upper(4, 10, "kSSSSSSSk", ".ksSSSsk");
18
- const torso = upper(4, 12, ".kCCCCCCk", ".kcCCCCCCk", ".kcCCCCCCk", ".kcCCCCCk", ".knnnnNnk", "kcCCkCCCCk", "kcCk.kCCCk", ".kk...kkk");
19
- const pauldron = upper(8, 11, "kaak", ".aaak", ".AAk");
20
- const tails = [
21
- upper(0, 10, "sSSS", "SS..", "s..."),
22
- upper(0, 9, "s...", ".SSS", "..sS"),
23
- upper(0, 10, "..SS", ".SSs", "Ss.."),
24
- upper(0, 10, "SSSS", "ssss"),
25
- ];
26
- const [tailA, tailB, tailC, tailStream] = tails;
27
- const hangingArm = upper(10, 14, "cck", "cck", "cck", "mm");
28
- const standingLegs = layer(5, 19, ".kpk.kpk", ".kPk.kppk", ".kPk.kppk", ".kPk.kppk", ".kPk.kppk", ".kbk.kbbk", "kbbk.kbbbk", "kkkk.kkkkk");
29
- const stride = (back, front) => layer(1, 19, `.....k${back}${back}${front}${front}k`, `....k${back}k..k${front}${front}k`, `...k${back}k....k${front}${front}k`, `..k${back}k......k${front}k`, ".kbk.......kpk", "kbk........kbbk", "kk.........kkkk");
30
- const passing = (raised, planted) => layer(4, 19, `..k${raised}${planted}${planted}k`, `.kbk${planted}${planted}k`, `kbbkk${planted}k`, `.kk.k${planted}${planted}k`, `....k${planted}k`, "....kbbk", "....kbbbk", "....kkkkk");
31
- const runLegs = [
32
- stride("P", "p"),
33
- passing("P", "p"),
34
- stride("p", "P"),
35
- passing("p", "P"),
36
- ];
37
- const armForward = upper(10, 13, "cc..", ".cck", "..mm");
38
- const armBack = upper(7, 13, "..cc", ".cc.", "mm..");
39
- const speedLines = [
40
- layer(0, 14, "vv", "", "", "", "v", "", "", "vv"),
41
- layer(0, 15, "v", "", "", "vv", "", "", "", "v"),
42
- ];
43
- const table = layer(12, 18, "xxxxxxxx", "......x.", "......x.", "......x.", "......x.", "......x.", "......x.", ".....xxx");
44
- const laptop = layer(13, 12, "....lL", "....lL", "....lL", "....lL", "....lL", "LLLLLL");
45
- const typing = [
46
- upper(10, 13, "cc..", ".cc.", "..cm", "...m"),
47
- upper(10, 13, "cc..", ".cc.", "..cm", "..m."),
48
- ];
49
- const codeBits = [
50
- layer(14, 9, "y", "", "..y", "", ".y"),
51
- layer(14, 7, ".y", "", "y", "", "..y"),
52
- layer(15, 5, "y", "", ".y", "", "y"),
53
- layer(14, 3, "..y", "", "y", "", ".y"),
54
- ];
55
- const cupLow = upper(10, 13, "cc..", ".ccm", "..QQ", "..Qo");
56
- const cupRaised = upper(10, 6, "...QQ", "...Qo", "..cm.", ".cc..", ".cc..", ".cc..");
57
- const steam = [
58
- layer(12, 12, ".v", "v.", ".v"),
59
- layer(12, 11, "v.", ".v", "v."),
60
- layer(14, 3, "v", ".v"),
61
- ];
62
- const appleAtMouth = upper(10, 5, "...G.", "...FF", "...FF", "..cm.", ".cc..", ".cc..", ".cc..");
63
- const appleInHand = upper(10, 13, "cc...", ".cmF.", "..FFG");
64
- const bitten = upper(10, 13, "cc...", ".cmF.", "..F.G");
65
- const dumbbellUp = upper(13, 0, "DDD", "DdD", "DDD", ".m.", ".c.", ".c.", ".c.", ".c.", ".c.", ".c.", ".c.", ".c.", "cc.");
66
- const dumbbellRacked = upper(13, 8, "DDD", "DdD", "DDD", ".m.", ".c.", "cc.");
67
- const sweat = [layer(16, 6, "u"), layer(16, 8, "u")];
68
- const reachUp = upper(13, 1, ".m", ".c", ".c", ".c", ".c", ".c", ".c", ".c", ".c", ".c", ".c", "cc");
69
- const reachForward = upper(10, 13, "ccccc", "....cmm");
70
- const swordRaised = upper(13, 0, ".t.", ".t.", ".t.", ".t.", "ggg", ".m.", ".c.", ".c.", ".c.", ".c.", ".c.", ".c.", "cc.");
71
- const sparkles = [
72
- layer(0, 1, ".y..........y", "", "", "", "", "", "y.................y"),
73
- layer(0, 3, "y.................y", "", "", "", "", "..y...........y"),
74
- ];
75
- const breathing = (legs, front) => [
76
- { legs, behind: [tailA], front },
77
- { legs, behind: [tailB], front },
78
- { legs, behind: [tailC], front, breathe: true },
79
- { legs, behind: [tailB], front, breathe: true },
80
- { legs, behind: [tailA], front },
81
- { legs, behind: [tailB], front, blink: true },
82
- { legs, behind: [tailC], front, breathe: true },
83
- { legs, behind: [tailB], front, breathe: true },
84
- ];
85
- const animations = {
86
- idle: {
87
- interval: 220,
88
- frames: breathing(standingLegs, [hangingArm]),
89
- },
90
- focus: {
91
- interval: 160,
92
- frames: codeBits.map((bits, index) => ({
93
- legs: standingLegs,
94
- behind: [index % 2 === 0 ? tailA : tailB, table],
95
- front: [laptop, typing[index % 2] ?? hangingArm, bits],
96
- breathe: index >= 2,
97
- })),
98
- },
99
- break: {
100
- interval: 380,
101
- frames: [
102
- { legs: standingLegs, behind: [tailA], front: [cupLow, steam[0]] },
103
- {
104
- legs: standingLegs,
105
- behind: [tailB],
106
- front: [cupLow, steam[1]],
107
- breathe: true,
108
- },
109
- {
110
- legs: standingLegs,
111
- behind: [tailC],
112
- front: [cupLow, steam[0]],
113
- breathe: true,
114
- },
115
- {
116
- legs: standingLegs,
117
- behind: [tailB],
118
- front: [cupRaised, steam[2]],
119
- blink: true,
120
- },
121
- {
122
- legs: standingLegs,
123
- behind: [tailA],
124
- front: [cupRaised, steam[2]],
125
- blink: true,
126
- },
127
- { legs: standingLegs, behind: [tailB], front: [cupLow, steam[1]] },
128
- ],
129
- },
130
- eat: {
131
- interval: 300,
132
- frames: [
133
- { legs: standingLegs, behind: [tailA], front: [appleInHand] },
134
- { legs: standingLegs, behind: [tailB], front: [appleAtMouth] },
135
- {
136
- legs: standingLegs,
137
- behind: [tailC],
138
- front: [appleAtMouth],
139
- breathe: true,
140
- },
141
- { legs: standingLegs, behind: [tailB], front: [bitten], breathe: true },
142
- ],
143
- },
144
- run: {
145
- interval: 110,
146
- frames: runLegs.map((legs, index) => ({
147
- legs,
148
- behind: [tailStream, speedLines[index % 2] ?? tailStream],
149
- front: [index % 2 === 0 ? armForward : armBack],
150
- dy: index % 2 === 0 ? 0 : -1,
151
- })),
152
- },
153
- lift: {
154
- interval: 420,
155
- frames: [
156
- { legs: standingLegs, behind: [tailA], front: [dumbbellRacked] },
157
- {
158
- legs: standingLegs,
159
- behind: [tailB],
160
- front: [dumbbellUp, sweat[0]],
161
- dy: -1,
162
- },
163
- {
164
- legs: standingLegs,
165
- behind: [tailC],
166
- front: [dumbbellUp, sweat[1]],
167
- dy: -1,
168
- },
169
- {
170
- legs: standingLegs,
171
- behind: [tailB],
172
- front: [dumbbellRacked],
173
- breathe: true,
174
- },
175
- ],
176
- },
177
- stretch: {
178
- interval: 520,
179
- frames: [
180
- { legs: standingLegs, behind: [tailA], front: [reachUp], dy: -1 },
181
- { legs: standingLegs, behind: [tailB], front: [reachUp], dy: -1 },
182
- {
183
- legs: standingLegs,
184
- behind: [tailC],
185
- front: [reachForward],
186
- breathe: true,
187
- },
188
- {
189
- legs: standingLegs,
190
- behind: [tailB],
191
- front: [reachForward],
192
- breathe: true,
193
- },
194
- ],
195
- },
196
- levelUp: {
197
- interval: 160,
198
- frames: sparkles.flatMap((burst, index) => [
199
- {
200
- legs: standingLegs,
201
- behind: [tailStream],
202
- front: [swordRaised, burst],
203
- dy: -1,
204
- },
205
- {
206
- legs: standingLegs,
207
- behind: [index === 0 ? tailA : tailC],
208
- front: [swordRaised, burst],
209
- },
210
- ]),
211
- },
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",
212
9
  };
213
- export const animationOf = (activity) => animations[activity];
214
- // Each gear slot upgrades once from level 2 and again 4 levels later, staggered
215
- // so every level from 2 to 9 changes exactly 1 piece.
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.
216
31
  export const gearOf = (level) => {
217
32
  const tier = (slot) => Math.min(Math.max(Math.floor((level - 2 - slot) / 4) + 1, 0), 2);
218
- return { weapon: tier(0), armor: tier(1), hood: tier(2), relic: tier(3) };
33
+ return { gadget: tier(0), top: tier(1), headwear: tier(2), back: tier(3) };
219
34
  };
220
- const weapons = [
221
- upper(2, 13, ".g.", "xxx", ".W.", ".W.", ".W.", ".W.", ".W.", ".w."),
222
- upper(2, 11, ".g.", ".g.", "xxx", ".w.", ".W.", ".W.", ".W.", ".W.", ".W.", ".W.", ".W.", ".w."),
223
- upper(1, 10, "..t", "..g", ".xxx", ".tw", ".tw", ".tw", ".tw", ".tw", ".tw", ".tw", ".tw", ".tw", "..t"),
224
- ];
225
- const hoods = [
226
- upper(0, 0, ""),
227
- upper(4, 4, "SSSSSSS"),
228
- upper(4, 0, ".Y....Y", ".YY..YY"),
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
+ };
56
+ };
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;
67
+ };
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],
74
+ });
75
+ const walkFeet = [
76
+ [
77
+ [2, 0],
78
+ [-1, 0],
79
+ ],
80
+ [
81
+ [0, 0],
82
+ [0, 1],
83
+ ],
84
+ [
85
+ [-3, 0],
86
+ [4, 0],
87
+ ],
88
+ [
89
+ [-2, 1],
90
+ [2, 0],
91
+ ],
229
92
  ];
230
- // The relic is a companion orb that bobs beside the character.
231
- const relics = [
232
- () => [],
233
- (bob) => [layer(17, 4 + bob, "t")],
234
- (bob) => [layer(16, 3 + bob, ".y.", "ytt", ".tt")],
93
+ const runFeet = [
94
+ [
95
+ [3, 1],
96
+ [-2, 2],
97
+ ],
98
+ [
99
+ [0, 0],
100
+ [0, 3],
101
+ ],
102
+ [
103
+ [-4, 2],
104
+ [5, 1],
105
+ ],
106
+ [
107
+ [-2, 3],
108
+ [2, 0],
109
+ ],
235
110
  ];
236
- const orbBob = [0, 0, 1, 1];
237
- const armorColors = [
238
- ["#6B4A35", "#4A3324"],
239
- ["#A3ACB8", "#6A7380"],
240
- ["#D9B45A", "#9C7A2E"],
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,
241
177
  ];
242
- export function paletteOf(level, palette) {
243
- const gear = gearOf(level);
244
- const [armor, armorShade] = armorColors[gear.armor] ?? armorColors[0];
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
+ ]),
247
+ ],
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
+ },
268
+ ],
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),
288
+ ],
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),
304
+ ],
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
+ ]),
355
+ ],
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
+ }
385
+ };
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);
426
+ };
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) {
245
511
  const { cloak, cloakShade, scarf, scarfShade, glow } = palette.sprite;
246
512
  return new Map([
247
- ["A", armorShade],
248
- ["a", armor],
249
- ["b", "#3B2D25"],
513
+ ["B", "#3E3226"],
514
+ ["b", "#5A4632"],
250
515
  ["C", cloak],
251
516
  ["c", cloakShade],
252
517
  ["D", "#50555F"],
253
518
  ["d", "#8A8F99"],
254
- ["E", glow],
519
+ ["e", "#2B2B2B"],
255
520
  ["F", "#C8473F"],
256
- ["f", "#241F2B"],
257
521
  ["G", "#5F8F4E"],
258
- ["g", "#B8913A"],
259
- ["H", cloak],
260
- ["h", cloakShade],
261
- ["k", "#15121A"],
522
+ ["g", "#D9B45A"],
523
+ ["h", "#6B4A2E"],
524
+ ["k", "#2B2B2B"],
262
525
  ["L", "#4A505B"],
263
526
  ["l", glow],
264
- ["m", "#C99272"],
265
- ["N", armor],
266
- ["n", "#4A3628"],
267
527
  ["o", "#5A3A25"],
268
- ["P", "#23252F"],
269
- ["p", "#2E3140"],
528
+ ["P", "#3E6870"],
529
+ ["p", "#4F8189"],
270
530
  ["Q", "#E6E0D4"],
531
+ ["q", "#C99872"],
271
532
  ["S", scarf],
272
- ["s", scarfShade],
533
+ ["s", "#E2B48C"],
534
+ ["T", "#DCD7CC"],
273
535
  ["t", glow],
274
- ["u", "#8FC6E8"],
536
+ ["U", "#B4AE9F"],
275
537
  ["v", "#9AA0A8"],
276
- ["W", "#8792A0"],
277
- ["w", "#C9D3DE"],
538
+ ["W", "#C8C8C8"],
539
+ ["w", "#F2F2F2"],
278
540
  ["x", "#3D414B"],
279
- ["Y", "#E8DCC0"],
280
541
  ["y", palette.soft],
542
+ ["z", scarfShade],
281
543
  ]);
282
544
  }
283
- // The pixel keys of 1 frame, row by row, "." where nothing is drawn. The
284
- // counter keeps running across frames, so the relic bobs on its own beat.
285
- export function composeFrame(activity, counter, level) {
286
- const { frames } = animations[activity];
287
- const frame = frames[counter % frames.length] ?? frames[0];
288
- const gear = gearOf(level);
289
- const canvas = Array.from({ length: spriteHeight }, () => Array.from({ length: spriteWidth }, () => "."));
290
- if (!frame) {
291
- return canvas.map((row) => row.join(""));
292
- }
293
- const lowerDy = frame.dy ?? 0;
294
- const upperDy = lowerDy + (frame.breathe ? 1 : 0);
295
- const head = frame.blink
296
- ? { ...hood, rows: hood.rows.map((row) => row.replaceAll("E", "f")) }
297
- : hood;
298
- const layers = [
299
- weapons[gear.weapon],
300
- ...(frame.behind ?? []),
301
- frame.legs,
302
- torso,
303
- neckScarf,
304
- head,
305
- hoods[gear.hood],
306
- pauldron,
307
- ...(frame.front ?? []),
308
- ...(relics[gear.relic]?.(orbBob[counter % orbBob.length] ?? 0) ?? []),
309
- ];
310
- for (const item of layers) {
311
- if (!item) {
312
- continue;
313
- }
314
- const dy = item.upper ? upperDy : lowerDy;
315
- for (const [offset, row] of item.rows.entries()) {
316
- const target = canvas[item.y + offset + dy];
317
- for (const [index, key] of [...row].entries()) {
318
- if (target && key !== "." && item.x + index < spriteWidth) {
319
- target[item.x + index] = key;
320
- }
321
- }
545
+ // The pixel keys of 1 frame, row by row, "." where nothing is drawn.
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;
322
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);
323
572
  }
324
573
  return canvas.map((row) => row.join(""));
325
574
  }
326
575
  // Pairs of pixel rows become 1 row of "▀"/"▄" cells, merged into styled runs.
327
576
  export function toSegments(frame, palette) {
328
577
  const lines = [];
578
+ const width = frame[0]?.length ?? 0;
329
579
  for (let y = 0; y < frame.length; y += 2) {
330
580
  const top = frame[y] ?? "";
331
581
  const bottom = frame[y + 1] ?? "";
332
582
  const line = [];
333
- for (let x = 0; x < spriteWidth; x++) {
583
+ for (let x = 0; x < width; x++) {
334
584
  const upperColor = palette.get(top[x] ?? ".");
335
585
  const lowerColor = palette.get(bottom[x] ?? ".");
336
586
  const cell = upperColor === undefined