@siegesailor/terminal-sigma 1.3.0 → 1.4.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.
@@ -1,16 +1,11 @@
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",
9
- };
10
- // 145 cm draws 20 pixels tall and 205 cm 26; the build follows the BMI.
11
- export function bodyOf(profile) {
1
+ // The pet lives on a stage as wide as its panel and 28 pixels tall. Each
2
+ // terminal cell draws 2 stacked pixels with "▀", foreground for the top and
3
+ // background for the bottom, so the stage takes 14 rows.
4
+ export const stageHeight = 28;
5
+ export const defaultLook = { size: 1, build: "average", gender: "other" };
6
+ export function lookOf(profile) {
12
7
  if (!profile) {
13
- return defaultBody;
8
+ return defaultLook;
14
9
  }
15
10
  const bmi = profile.weight / (profile.height / 100) ** 2;
16
11
  let build = "average";
@@ -21,249 +16,152 @@ export function bodyOf(profile) {
21
16
  build = "broad";
22
17
  }
23
18
  return {
24
- height: Math.min(Math.max(Math.round(20 + (profile.height - 145) / 10), 20), 26),
19
+ size: Math.min(Math.max(Math.floor((profile.height - 150) / 12), 0), 3),
25
20
  build,
26
21
  gender: profile.gender,
22
+ goal: profile.goal,
27
23
  };
28
24
  }
25
+ export const noBadges = { meal: false, workout: false, focus: false };
29
26
  // Each equipment slot upgrades once from level 2 and again 4 levels later,
30
27
  // staggered so every level from 2 to 9 changes exactly 1 piece.
31
28
  export const gearOf = (level) => {
32
29
  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) };
30
+ return { gadget: tier(0), wear: tier(1), headwear: tier(2), aura: tier(3) };
34
31
  };
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
- };
32
+ // The pet grows twice: a hatchling below level 4, an adult from level 7.
33
+ export const growthOf = (level) => level >= 7 ? 2 : level >= 4 ? 1 : 0;
34
+ const widthOf = { slim: 13, average: 15, broad: 18 };
35
+ const shapeOf = (look, level, squash = 0) => {
36
+ const growth = growthOf(level) - 1;
37
+ const width = widthOf[look.build] + growth * 2 - squash;
38
+ const height = 9 + look.size + growth * 2 + squash;
39
+ return { width, height, top: height + 1 };
56
40
  };
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;
41
+ const span = (put, from, to, y, key) => {
42
+ for (let x = from; x <= to; x++) {
43
+ put(x, y, key);
44
+ }
67
45
  };
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
- ],
92
- ];
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
- ],
110
- ];
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
- };
46
+ const icon = (rows, x, y) => (put) => {
47
+ for (const [offset, row] of rows.entries()) {
48
+ for (const [index, key] of [...row].entries()) {
49
+ put(x + index, y - offset, key);
50
+ }
51
+ }
125
52
  };
126
- const sparkle = (points) => (put) => {
53
+ const sparkles = (points) => (put, shape) => {
127
54
  for (const [x, y] of points) {
128
- put(x, y, "y");
55
+ put(x < 0 ? x : shape.width - 1 + x, y + shape.top, "y");
129
56
  }
130
57
  };
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");
58
+ const laptop = (put, { width }) => {
59
+ span(put, width + 3, width + 8, 0, "x");
60
+ for (let y = 1; y <= 2; y++) {
61
+ put(width + 7, y, "x");
138
62
  }
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");
63
+ span(put, width + 3, width + 7, 3, "L");
64
+ for (let y = 4; y <= 7; y++) {
65
+ put(width + 6, y, "l");
66
+ put(width + 7, y, "L");
145
67
  }
146
68
  };
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
- }
69
+ const held = (rows, dy) => (put, shape) => {
70
+ icon(rows, shape.width + 2, dy(shape))(put, shape, 0);
158
71
  };
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;
72
+ // Where the pet stands and what it does on every tick of `counter`.
73
+ function sceneOf(activity, counter, width, shape, mood) {
74
+ const home = 3;
75
+ const minimum = 3;
76
+ const maximum = Math.max(width - shape.width - 3, minimum + 1);
77
+ const travel = maximum - minimum;
78
+ const tick = counter % 4;
79
+ if (activity === "idle" && mood === "sleepy") {
80
+ // Napping in place: breathing, with Zs drifting up.
81
+ return {
82
+ x: Math.floor((width - shape.width) / 2),
83
+ facing: 1,
84
+ pose: { squash: tick < 2 ? 0 : 1, eyes: "closed" },
85
+ props: [
86
+ (put, { width: body, top }) => {
87
+ const rise = counter % 6;
88
+ icon(["zzzz", "..z.", ".z..", "zzzz"], body - 2 + rise, top + 4 + rise)(put, shape, 0);
89
+ },
90
+ ],
91
+ };
92
+ }
93
+ if (activity === "idle") {
94
+ // Wander: pause, walk across, pause and look about, walk back.
95
+ const pause = 6;
96
+ const cycle = 2 * (pause + travel);
97
+ const at = counter % cycle;
98
+ const isHappy = mood === "happy";
99
+ const walk = (step) => ({
100
+ step: step % 2 === 0 ? 1 : 2,
101
+ look: 1,
102
+ lift: isHappy && step % 2 === 1 ? 1 : 0,
103
+ eyes: isHappy ? "happy" : "open",
104
+ });
105
+ const rest = (moment) => ({
106
+ squash: Math.floor(moment / 2) % 2,
107
+ eyes: isHappy ? "happy" : moment === 4 ? "closed" : "open",
108
+ arms: isHappy && moment % 2 === 0 ? ["up", "up"] : undefined,
109
+ });
110
+ if (at < pause) {
219
111
  return {
220
- x: maximum - step,
221
- facing: -1,
222
- pose: (d) => stride(d, walkFeet, step, 1),
112
+ x: minimum,
113
+ facing: at === 2 || at === 3 ? -1 : 1,
114
+ pose: rest(at),
223
115
  };
224
116
  }
117
+ if (at < pause + travel) {
118
+ return { x: minimum + at - pause, facing: 1, pose: walk(at - pause) };
119
+ }
120
+ if (at < 2 * pause + travel) {
121
+ const moment = at - pause - travel;
122
+ return {
123
+ x: maximum,
124
+ facing: moment === 2 || moment === 3 ? 1 : -1,
125
+ pose: rest(moment),
126
+ };
127
+ }
128
+ const step = at - 2 * pause - travel;
129
+ return { x: maximum - step, facing: -1, pose: walk(step) };
130
+ }
131
+ switch (activity) {
225
132
  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;
133
+ const lap = Math.max(Math.ceil(travel / 2), 1);
134
+ const at = counter % (2 * lap);
135
+ const forward = at < lap;
136
+ const step = forward ? at : at - lap;
231
137
  return {
232
138
  x: forward
233
139
  ? Math.min(minimum + step * 2, maximum)
234
140
  : Math.max(maximum - step * 2, minimum),
235
141
  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
- ],
142
+ pose: {
143
+ step: counter % 2 === 0 ? 1 : 2,
144
+ lift: counter % 2,
145
+ look: 1,
146
+ arms: counter % 2 === 0 ? ["up", "down"] : ["down", "up"],
147
+ },
148
+ props: [icon(["vv.", "...", ".vv"], -6, 6)],
248
149
  };
249
150
  }
250
151
  case "focus": {
251
- const step = counter % 4;
252
152
  return {
253
153
  x: home,
254
154
  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
- }),
155
+ pose: {
156
+ look: 1,
157
+ squash: tick < 2 ? 0 : 1,
158
+ arms: ["down", tick % 2 === 0 ? "forward" : "mid"],
159
+ },
261
160
  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");
161
+ laptop,
162
+ (put, { width: body }) => {
163
+ put(body + 5 + (tick % 2), 9 + tick, "y");
164
+ put(body + 7 - (tick % 2), 9 + ((tick + 2) % 4), "y");
267
165
  },
268
166
  ],
269
167
  };
@@ -271,86 +169,94 @@ function sceneOf(activity, counter, width) {
271
169
  case "break": {
272
170
  const step = counter % 6;
273
171
  const sipping = step >= 4;
274
- const hand = (d) => sipping ? [2, d.neck + 1 - d.shoulder] : [2, -d.arm + 2];
275
172
  return {
276
173
  x: home,
277
174
  facing: 1,
278
- pose: (d) => ({
279
- ...standing(d, hand(d)),
280
- bob: step % 2,
281
- blink: sipping,
282
- }),
175
+ pose: {
176
+ look: 1,
177
+ squash: step % 2,
178
+ eyes: sipping ? "closed" : "open",
179
+ arms: ["down", sipping ? "up" : "mid"],
180
+ },
283
181
  props: [
284
- cup((d) => {
285
- const [x, y] = handAt(d, hand(d));
286
- return [x + 1, y];
287
- }, sipping ? undefined : step),
182
+ held(["QQ", "Qc"], (shape) => sipping ? shape.top - 1 : Math.ceil(shape.height / 2) + 2),
183
+ ...(sipping
184
+ ? []
185
+ : [
186
+ held(step % 2 === 0 ? [".v", "v."] : ["v.", ".v"], (shape) => Math.ceil(shape.height / 2) + 5),
187
+ ]),
288
188
  ],
289
189
  };
290
190
  }
291
191
  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];
192
+ const atMouth = tick === 1 || tick === 2;
295
193
  return {
296
194
  x: home,
297
195
  facing: 1,
298
- pose: (d) => ({ ...standing(d, hand(d)), bob: step === 2 ? 1 : 0 }),
196
+ pose: {
197
+ look: 1,
198
+ squash: tick === 2 ? 1 : 0,
199
+ eyes: tick === 2 ? "happy" : "open",
200
+ arms: ["down", atMouth ? "up" : "mid"],
201
+ },
299
202
  props: [
300
- apple((d) => {
301
- const [x, y] = handAt(d, hand(d));
302
- return [x + 1, y];
303
- }, step === 3),
203
+ held(tick === 3 ? [".G", "F.", "FF"] : [".G", "FF", "FF"], (shape) => atMouth ? shape.top - 2 : Math.ceil(shape.height / 2) + 2),
304
204
  ],
305
205
  };
306
206
  }
307
207
  case "lift": {
308
- const step = counter % 4;
309
- const hand = (d) => step === 2 ? [1, d.arm] : [2, step === 0 ? -d.arm + 3 : 1];
208
+ const up = tick === 1 || tick === 2;
310
209
  return {
311
210
  x: home,
312
211
  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)))],
212
+ pose: {
213
+ arms: up ? ["up", "up"] : ["mid", "mid"],
214
+ lift: tick === 2 ? 1 : 0,
215
+ squash: up ? 0 : 1,
216
+ },
217
+ props: [
218
+ (put, shape) => {
219
+ const y = up ? shape.top + 3 : Math.ceil(shape.height / 2) + 4;
220
+ span(put, -3, shape.width + 2, y, "d");
221
+ icon(["DD", "DD", "DD"], -4, y + 1)(put, shape, 0);
222
+ icon(["DD", "DD", "DD"], shape.width + 2, y + 1)(put, shape, 0);
223
+ },
224
+ ],
319
225
  };
320
226
  }
321
227
  case "stretch": {
322
- const step = counter % 4;
323
- const up = step < 2;
324
228
  return {
325
229
  x: home,
326
230
  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
- }),
231
+ pose: {
232
+ arms: tick < 2 ? ["up", "up"] : ["mid", "mid"],
233
+ squash: tick < 2 ? -1 : 1,
234
+ eyes: tick === 1 ? "closed" : "open",
235
+ },
332
236
  };
333
237
  }
334
238
  case "levelUp": {
335
- const step = counter % 4;
336
- const lift = [0, 2, 3, 1][step] ?? 0;
337
239
  return {
338
240
  x: home,
339
241
  facing: 1,
340
- pose: (d) => ({ ...standing(d, [1, d.arm]), lift }),
242
+ pose: {
243
+ arms: ["up", "up"],
244
+ lift: [0, 2, 3, 1][tick] ?? 0,
245
+ eyes: "happy",
246
+ },
341
247
  props: [
342
- sparkle(step % 2 === 0
248
+ sparkles(tick % 2 === 0
343
249
  ? [
344
- [-5, 20],
345
- [5, 22],
346
- [-6, 12],
347
- [6, 14],
250
+ [-3, 2],
251
+ [3, 4],
252
+ [-4, -3],
253
+ [4, -1],
348
254
  ]
349
255
  : [
350
- [-6, 22],
351
- [6, 20],
352
- [-5, 14],
353
- [5, 12],
256
+ [-4, 4],
257
+ [4, 2],
258
+ [-3, -1],
259
+ [3, -3],
354
260
  ]),
355
261
  ],
356
262
  };
@@ -358,7 +264,7 @@ function sceneOf(activity, counter, width) {
358
264
  }
359
265
  }
360
266
  export const intervalOf = (activity) => ({
361
- idle: 200,
267
+ idle: 220,
362
268
  focus: 180,
363
269
  break: 380,
364
270
  eat: 300,
@@ -369,206 +275,220 @@ export const intervalOf = (activity) => ({
369
275
  })[activity];
370
276
  // How many ticks one loop of an activity takes on a stage of `width`.
371
277
  export function cycleOf(activity, width) {
372
- const walk = Math.max(width - 7, 6) - 5;
278
+ const travel = Math.max(width - 21, 4);
373
279
  if (activity === "idle") {
374
- return 2 * (8 + walk);
280
+ return 2 * (6 + travel);
375
281
  }
376
282
  if (activity === "run") {
377
- return 2 * Math.max(Math.ceil(walk / 2), 1);
283
+ return 2 * Math.max(Math.ceil(travel / 2), 1);
378
284
  }
379
285
  return activity === "break" ? 6 : 4;
380
286
  }
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);
287
+ // Behind the body: a cape that flares and ripples, or sparkles of the aura.
288
+ function paintBehind({ put, shape, gear, counter }) {
289
+ if (gear.wear === 2) {
290
+ for (let y = 2; y <= shape.top - 2; y++) {
291
+ const flare = Math.floor((shape.top - y) / 3) + ((counter + y) % 4 < 2 ? 0 : 1);
292
+ span(put, -1 - flare, -1, y, y === 2 ? "s" : "S");
293
+ }
397
294
  }
398
- const wrist = points.at(-2);
399
- if (isFront && gear.gadget === 2 && wrist && points.length > 2) {
400
- upper(wrist[0], wrist[1], "t");
295
+ if (gear.aura === 2) {
296
+ const sway = counter % 2;
297
+ put(-3 - sway, shape.top - 1, "y");
298
+ put(shape.width + 2 + sway, shape.top - 3, "y");
299
+ put(-2, 3 + sway, "y");
401
300
  }
402
301
  }
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");
302
+ // 4 short legs, like the Claude Code mascot; sneakers for an endurance goal.
303
+ function paintLegs({ put, shape, look, pose }) {
304
+ const columns = [1, 3, shape.width - 4, shape.width - 2];
305
+ const foot = look.goal === "endurance" ? "w" : "o";
306
+ for (const [index, x] of columns.entries()) {
307
+ const isLifted = (pose.step === 1 && index % 2 === 0) ||
308
+ (pose.step === 2 && index % 2 === 1);
309
+ if (!isLifted) {
310
+ put(x, 0, foot);
410
311
  }
312
+ put(x, 1, "o");
411
313
  }
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
- }
314
+ }
315
+ function paintBody({ put, shape, look, gear }) {
316
+ const { width, top } = shape;
317
+ for (let y = 2; y <= top; y++) {
318
+ const isEdge = y === 2 || y === top;
319
+ span(put, isEdge ? 1 : 0, isEdge ? width - 2 : width - 1, y, y === 2 ? "o" : "O");
320
+ }
321
+ span(put, 1, 2, top - 1, "h");
322
+ if (look.goal === "loseFat") {
323
+ span(put, 0, width - 1, top - 1, "w");
324
+ }
325
+ else if (look.goal === "maintain") {
326
+ const middle = Math.floor(width / 2);
327
+ icon(["r.r", "rrr", ".r."], middle - 1, 6)(put, shape, 0);
328
+ }
329
+ if (gear.wear === 1) {
330
+ span(put, 0, width - 1, top - 5, "S");
331
+ put(-1, top - 5, "S");
332
+ put(-2, top - 6, "s");
417
333
  }
418
334
  }
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);
335
+ function paintFace({ put, shape, pose, counter }) {
336
+ const look = pose.look ?? 0;
337
+ const eyeY = shape.top - 3;
338
+ const eyes = [
339
+ Math.round(shape.width * 0.3) + look,
340
+ Math.round(shape.width * 0.7) - 1 + look,
341
+ ];
342
+ for (const x of eyes) {
343
+ if (pose.eyes === "closed") {
344
+ span(put, x, x + 1, eyeY, "e");
424
345
  }
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");
346
+ else if (pose.eyes === "happy") {
347
+ put(x - 1, eyeY, "e");
348
+ put(x, eyeY + 1, "e");
349
+ put(x + 1, eyeY, "e");
350
+ put(x, eyeY - 2, "r");
351
+ }
352
+ else {
353
+ put(x, eyeY, "e");
354
+ put(x, eyeY + 1, counter % 23 === 0 ? "O" : "e");
446
355
  }
447
- upper(d.right - 1, d.shoulder, "S");
448
356
  }
449
357
  }
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");
358
+ // Side nubs; a muscle goal makes them bigger.
359
+ function paintArms({ put, shape, look, pose }) {
360
+ const size = look.goal === "buildMuscle" ? 3 : 2;
361
+ const [back, front] = pose.arms ?? ["down", "down"];
362
+ const rowOf = (arm) => ({
363
+ down: 3 + size - 1,
364
+ mid: Math.ceil(shape.height / 2) + size,
365
+ up: shape.top + size - 1,
366
+ forward: Math.ceil(shape.height / 2) + size,
367
+ })[arm];
368
+ const nub = (x, arm) => {
369
+ const reach = arm === "forward" ? 1 : 0;
370
+ for (let row = 0; row < size; row++) {
371
+ span(put, x + reach, x + reach + size - 1, rowOf(arm) - row, row === size - 1 ? "o" : "O");
465
372
  }
466
- span(upper, -2, -2, n - 1, "h");
467
- upper(-2, n, "h");
468
- upper(0, n + 4, "h");
373
+ };
374
+ nub(-size, back);
375
+ nub(shape.width, front);
376
+ }
377
+ // Gender sets an ornament at the back of the head, level the gear on top.
378
+ function paintHead({ put, shape, look, gear, counter }) {
379
+ const { width, top } = shape;
380
+ if (look.gender === "female") {
381
+ icon(["r.r", "rrr", "r.r"], 0, top + 3)(put, shape, 0);
469
382
  }
470
- else if (body.gender === "other") {
471
- for (let y = n + 1; y <= n + 3; y++) {
472
- upper(-3, y, "h");
473
- }
383
+ else if (look.gender === "male") {
384
+ put(1, top + 1, "O");
385
+ put(2, top + 2, "O");
386
+ put(3, top + 1, "O");
474
387
  }
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");
388
+ else {
389
+ put(2, top + 1, "G");
390
+ put(1, top + 2, "G");
391
+ put(3, top + 3, "G");
392
+ }
393
+ if (gear.gadget >= 1) {
394
+ span(put, 5, width - 2, top + 1, "k");
395
+ icon(["S", "S"], -1, top - 1)(put, shape, 0);
396
+ icon(["S", "S"], width, top - 1)(put, shape, 0);
397
+ }
398
+ if (gear.gadget === 2) {
399
+ put(width - 3, top + 2, "k");
400
+ put(width - 3, top + 3, counter % 2 === 0 ? "t" : "y");
487
401
  }
488
402
  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");
403
+ span(put, 5, width - 3, top + 1, "S");
404
+ span(put, 6, width - 4, top + 2, "S");
405
+ span(put, width - 2, width, top + 1, "s");
492
406
  }
493
407
  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");
408
+ span(put, 6, width - 3, top + 1, "g");
409
+ put(6, top + 2, "g");
410
+ put(Math.floor((width + 3) / 2), top + 2, "g");
411
+ put(width - 3, top + 2, "g");
412
+ put(Math.floor((width + 3) / 2), top + 1, "t");
413
+ }
414
+ if (gear.aura >= 1) {
415
+ const bob = counter % 4 < 2 ? 0 : 1;
416
+ put(width + 4, top + 2 + bob, "t");
417
+ put(width + 4, top + 3 + bob, "y");
498
418
  }
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
419
  }
510
420
  export function paletteOf(palette) {
511
- const { cloak, cloakShade, scarf, scarfShade, glow } = palette.sprite;
421
+ const { cloak, cloakShade, glow } = palette.sprite;
512
422
  return new Map([
513
- ["B", "#3E3226"],
514
- ["b", "#5A4632"],
515
- ["C", cloak],
516
- ["c", cloakShade],
423
+ ["c", "#5A3A25"],
517
424
  ["D", "#50555F"],
518
425
  ["d", "#8A8F99"],
519
- ["e", "#2B2B2B"],
426
+ ["e", "#1E1E1E"],
520
427
  ["F", "#C8473F"],
521
- ["G", "#5F8F4E"],
428
+ ["G", "#6FA35A"],
522
429
  ["g", "#D9B45A"],
523
- ["h", "#6B4A2E"],
430
+ ["h", palette.soft],
524
431
  ["k", "#2B2B2B"],
525
432
  ["L", "#4A505B"],
526
433
  ["l", glow],
527
- ["o", "#5A3A25"],
528
- ["P", "#3E6870"],
529
- ["p", "#4F8189"],
434
+ ["O", palette.accent],
435
+ ["o", palette.deep],
530
436
  ["Q", "#E6E0D4"],
531
- ["q", "#C99872"],
532
- ["S", scarf],
533
- ["s", "#E2B48C"],
534
- ["T", "#DCD7CC"],
437
+ ["r", "#E87A90"],
438
+ ["S", cloak],
439
+ ["s", cloakShade],
440
+ ["T", "#E5533D"],
535
441
  ["t", glow],
536
- ["U", "#B4AE9F"],
537
442
  ["v", "#9AA0A8"],
538
- ["W", "#C8C8C8"],
539
443
  ["w", "#F2F2F2"],
540
444
  ["x", "#3D414B"],
541
445
  ["y", palette.soft],
542
- ["z", scarfShade],
446
+ ["z", "#C8C8C8"],
543
447
  ]);
544
448
  }
545
449
  // The pixel keys of 1 frame, row by row, "." where nothing is drawn.
546
- export function composeScene({ activity, counter, level, body, width, }) {
450
+ export function composeScene({ activity, counter, level, look, mood, badges, width, }) {
547
451
  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)];
452
+ const plain = (column, y, key) => {
453
+ const row = canvas[stageHeight - 1 - y];
555
454
  if (row && column >= 0 && column < width && key !== ".") {
556
455
  row[column] = key;
557
456
  }
558
457
  };
559
- drawCharacter({
458
+ const baseShape = shapeOf(look, level);
459
+ const scene = sceneOf(activity, counter, width, baseShape, mood);
460
+ const shape = shapeOf(look, level, scene.pose.squash ?? 0);
461
+ const lift = scene.pose.lift ?? 0;
462
+ // Local x runs from the body's left edge toward where the pet faces.
463
+ const put = (x, y, key) => {
464
+ const column = scene.facing === 1 ? scene.x + x : scene.x + baseShape.width - 1 - x;
465
+ plain(column, y + lift, key);
466
+ };
467
+ const paint = {
560
468
  put,
561
- upper(x, y, key) {
562
- put(x, y - (pose.bob ?? 0), key);
563
- },
564
- d,
565
- body,
469
+ shape,
470
+ look,
566
471
  gear: gearOf(level),
567
- pose,
472
+ pose: scene.pose,
568
473
  counter,
569
- });
474
+ };
475
+ paintBehind(paint);
476
+ paintLegs(paint);
477
+ paintBody(paint);
478
+ paintFace(paint);
479
+ paintArms(paint);
480
+ paintHead(paint);
570
481
  for (const draw of scene.props ?? []) {
571
- draw(put, d);
482
+ draw(put, shape, counter);
483
+ }
484
+ // Today's badges float in the top right corner, whatever the pet does.
485
+ const icons = [
486
+ badges.focus ? ["GGG", "TTT", "TTT"] : undefined,
487
+ badges.meal ? [".G.", "FFF", ".F."] : undefined,
488
+ badges.workout ? ["D.D", "DdD", "D.D"] : undefined,
489
+ ].filter((rows) => rows !== undefined);
490
+ for (const [index, rows] of icons.entries()) {
491
+ icon(rows, width - 4 - index * 4, stageHeight - 2)(plain, shape, 0);
572
492
  }
573
493
  return canvas.map((row) => row.join(""));
574
494
  }