@siegesailor/terminal-sigma 1.4.1 → 1.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +46 -29
- package/build/app.js +87 -36
- package/build/app.js.map +1 -1
- package/build/command-line-interface.js +15 -1
- package/build/command-line-interface.js.map +1 -1
- package/build/components/character.js +26 -10
- package/build/components/character.js.map +1 -1
- package/build/components/choice.js +11 -6
- package/build/components/choice.js.map +1 -1
- package/build/components/entry-form.js +5 -3
- package/build/components/entry-form.js.map +1 -1
- package/build/content.js +107 -36
- package/build/content.js.map +1 -1
- package/build/health.js +5 -2
- package/build/health.js.map +1 -1
- package/build/i18n.js +111 -42
- package/build/i18n.js.map +1 -1
- package/build/pixel-art.js +1122 -371
- package/build/pixel-art.js.map +1 -1
- package/build/progress.js +2 -0
- package/build/progress.js.map +1 -1
- package/build/update.js +61 -0
- package/build/update.js.map +1 -0
- package/package.json +1 -1
package/build/pixel-art.js
CHANGED
|
@@ -1,494 +1,1245 @@
|
|
|
1
|
-
// The
|
|
2
|
-
// terminal cell draws 2 stacked pixels with "▀", foreground for the top
|
|
3
|
-
// background for the bottom, so the stage takes
|
|
4
|
-
export const stageHeight =
|
|
5
|
-
|
|
1
|
+
// The character stands on a stage as wide as its panel and 40 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 20 rows.
|
|
4
|
+
export const stageHeight = 40;
|
|
5
|
+
// 10 frames a second; slower motions hold a pose for several frames.
|
|
6
|
+
export const frameMilliseconds = 100;
|
|
7
|
+
export const defaultLook = {
|
|
8
|
+
height: 29,
|
|
9
|
+
build: "average",
|
|
10
|
+
gender: "other",
|
|
11
|
+
style: "desk",
|
|
12
|
+
hair: "dark",
|
|
13
|
+
isMuscular: false,
|
|
14
|
+
};
|
|
15
|
+
// Body fat that counts as lean, soft, and heavy, by gender.
|
|
16
|
+
const fatBands = {
|
|
17
|
+
female: [21, 32, 38],
|
|
18
|
+
male: [13, 22, 28],
|
|
19
|
+
other: [17, 27, 33],
|
|
20
|
+
};
|
|
21
|
+
// Body fat tells muscle from fat at the same weight; without it, BMI decides.
|
|
22
|
+
function buildOf(profile) {
|
|
23
|
+
const bmi = profile.weight / (profile.height / 100) ** 2;
|
|
24
|
+
if (profile.bodyFat === undefined) {
|
|
25
|
+
if (bmi < 19.5) {
|
|
26
|
+
return "slim";
|
|
27
|
+
}
|
|
28
|
+
if (bmi >= 30) {
|
|
29
|
+
return "heavy";
|
|
30
|
+
}
|
|
31
|
+
return bmi >= 25 ? "stocky" : "average";
|
|
32
|
+
}
|
|
33
|
+
const [lean, soft, heavy] = fatBands[profile.gender];
|
|
34
|
+
if (profile.bodyFat < lean) {
|
|
35
|
+
return bmi >= 23 ? "athletic" : "slim";
|
|
36
|
+
}
|
|
37
|
+
if (profile.bodyFat >= heavy) {
|
|
38
|
+
return "heavy";
|
|
39
|
+
}
|
|
40
|
+
return profile.bodyFat >= soft ? "stocky" : "average";
|
|
41
|
+
}
|
|
42
|
+
// 145 cm draws 26 pixels tall and 205 cm 34.
|
|
6
43
|
export function lookOf(profile) {
|
|
7
44
|
if (!profile) {
|
|
8
45
|
return defaultLook;
|
|
9
46
|
}
|
|
10
|
-
const
|
|
11
|
-
let
|
|
12
|
-
if (
|
|
13
|
-
|
|
47
|
+
const build = buildOf(profile);
|
|
48
|
+
let hair = "dark";
|
|
49
|
+
if (profile.age >= 70) {
|
|
50
|
+
hair = "white";
|
|
14
51
|
}
|
|
15
|
-
else if (
|
|
16
|
-
|
|
52
|
+
else if (profile.age >= 55) {
|
|
53
|
+
hair = "grey";
|
|
17
54
|
}
|
|
18
55
|
return {
|
|
19
|
-
|
|
56
|
+
height: Math.min(Math.max(Math.round(26 + (profile.height - 145) / 7.5), 24), 35),
|
|
20
57
|
build,
|
|
21
58
|
gender: profile.gender,
|
|
22
|
-
|
|
59
|
+
style: profile.workStyle,
|
|
60
|
+
hair,
|
|
61
|
+
isMuscular: build === "athletic" || profile.goal === "buildMuscle",
|
|
23
62
|
};
|
|
24
63
|
}
|
|
25
|
-
export const noBadges = { meal: false, workout: false, focus: false };
|
|
26
64
|
// Each equipment slot upgrades once from level 2 and again 4 levels later,
|
|
27
65
|
// staggered so every level from 2 to 9 changes exactly 1 piece.
|
|
28
66
|
export const gearOf = (level) => {
|
|
29
67
|
const tier = (slot) => Math.min(Math.max(Math.floor((level - 2 - slot) / 4) + 1, 0), 2);
|
|
30
|
-
return { gadget: tier(0), wear: tier(1), headwear: tier(2),
|
|
68
|
+
return { gadget: tier(0), wear: tier(1), headwear: tier(2), carry: tier(3) };
|
|
31
69
|
};
|
|
32
|
-
// The
|
|
33
|
-
|
|
34
|
-
const
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
const height = 9 + look.size + growth * 2 + squash;
|
|
39
|
-
return { width, height, top: height + 1 };
|
|
70
|
+
// The feet stand on row 2, in the middle of their shadow.
|
|
71
|
+
const ground = 2;
|
|
72
|
+
const frames = {
|
|
73
|
+
female: [4, 3, 5],
|
|
74
|
+
male: [5, 4, 4],
|
|
75
|
+
other: [4, 4, 4],
|
|
40
76
|
};
|
|
77
|
+
// A 7-row head and a neck take 8 of the height; legs take 55% of the rest.
|
|
78
|
+
function bodyOf(look) {
|
|
79
|
+
const span = look.height - 8;
|
|
80
|
+
const legs = Math.round(span * 0.55);
|
|
81
|
+
const torso = span - legs;
|
|
82
|
+
let [chest, waist, hips] = frames[look.gender];
|
|
83
|
+
switch (look.build) {
|
|
84
|
+
case "slim": {
|
|
85
|
+
chest -= look.gender === "male" ? 1 : 0;
|
|
86
|
+
waist = Math.max(waist - 1, 2);
|
|
87
|
+
hips = Math.max(hips - 1, 3);
|
|
88
|
+
break;
|
|
89
|
+
}
|
|
90
|
+
case "athletic": {
|
|
91
|
+
chest += 1;
|
|
92
|
+
break;
|
|
93
|
+
}
|
|
94
|
+
case "stocky": {
|
|
95
|
+
waist += 1;
|
|
96
|
+
hips += 1;
|
|
97
|
+
break;
|
|
98
|
+
}
|
|
99
|
+
case "heavy": {
|
|
100
|
+
chest += 1;
|
|
101
|
+
waist += 3;
|
|
102
|
+
hips += 2;
|
|
103
|
+
break;
|
|
104
|
+
}
|
|
105
|
+
case "average": {
|
|
106
|
+
break;
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
if (look.isMuscular && look.build !== "athletic") {
|
|
110
|
+
chest += 1;
|
|
111
|
+
}
|
|
112
|
+
let thigh = Math.max(hips - 1, 3);
|
|
113
|
+
if (look.build === "heavy") {
|
|
114
|
+
thigh = hips;
|
|
115
|
+
}
|
|
116
|
+
else if (look.build === "slim") {
|
|
117
|
+
thigh = Math.max(hips - 1, 2);
|
|
118
|
+
}
|
|
119
|
+
const hip = ground + legs;
|
|
120
|
+
const shoulder = hip + torso - 1;
|
|
121
|
+
const chin = shoulder + 2;
|
|
122
|
+
return {
|
|
123
|
+
hip,
|
|
124
|
+
shoulder,
|
|
125
|
+
chin,
|
|
126
|
+
top: chin + 6,
|
|
127
|
+
legs,
|
|
128
|
+
knee: ground + Math.floor(legs / 2),
|
|
129
|
+
shin: ground + Math.floor(legs / 4),
|
|
130
|
+
chest,
|
|
131
|
+
waist,
|
|
132
|
+
hips,
|
|
133
|
+
thigh,
|
|
134
|
+
calf: Math.max(thigh - 1, 2),
|
|
135
|
+
ankle: Math.max(thigh - 2, 2),
|
|
136
|
+
arm: look.isMuscular || look.build === "heavy" ? 3 : 2,
|
|
137
|
+
reach: torso + 3,
|
|
138
|
+
};
|
|
139
|
+
}
|
|
41
140
|
const span = (put, from, to, y, key) => {
|
|
42
|
-
for (let x = from; x <= to; x++) {
|
|
141
|
+
for (let x = Math.min(from, to); x <= Math.max(from, to); x++) {
|
|
43
142
|
put(x, y, key);
|
|
44
143
|
}
|
|
45
144
|
};
|
|
46
|
-
const
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
145
|
+
const pointsOf = ([x0, y0], [x1, y1]) => {
|
|
146
|
+
const points = [];
|
|
147
|
+
const steps = Math.max(Math.abs(x1 - x0), Math.abs(y1 - y0), 1);
|
|
148
|
+
for (let step = 0; step <= steps; step++) {
|
|
149
|
+
points.push([
|
|
150
|
+
Math.round(x0 + ((x1 - x0) * step) / steps),
|
|
151
|
+
Math.round(y0 + ((y1 - y0) * step) / steps),
|
|
152
|
+
]);
|
|
153
|
+
}
|
|
154
|
+
return points;
|
|
155
|
+
};
|
|
156
|
+
// Where a hand rests with the arm hanging, nudged by `swing`.
|
|
157
|
+
const hanging = (b, side, swing = 0) => [
|
|
158
|
+
side * (b.chest + 1),
|
|
159
|
+
b.shoulder - b.reach + 1 + swing,
|
|
160
|
+
];
|
|
161
|
+
const breathing = (t, every = 6) => Math.floor(t / every) % 2;
|
|
162
|
+
// The stage edges the character walks between.
|
|
163
|
+
const marginOf = (width) => {
|
|
164
|
+
const minimum = 9;
|
|
165
|
+
return { minimum, maximum: Math.max(width - 10, minimum + 1) };
|
|
166
|
+
};
|
|
167
|
+
export const homeOf = (width) => Math.floor(width / 2);
|
|
168
|
+
// Walks a path of stops at `speed` pixels a tick, returning the position
|
|
169
|
+
// and heading at tick `t`, or undefined once the path is done.
|
|
170
|
+
function along(stops, speed, t) {
|
|
171
|
+
let remaining = t;
|
|
172
|
+
for (let index = 1; index < stops.length; index++) {
|
|
173
|
+
const from = stops[index - 1] ?? 0;
|
|
174
|
+
const to = stops[index] ?? 0;
|
|
175
|
+
const ticks = Math.ceil(Math.abs(to - from) / speed);
|
|
176
|
+
if (remaining < ticks) {
|
|
177
|
+
const facing = to >= from ? 1 : -1;
|
|
178
|
+
return {
|
|
179
|
+
x: from + facing * Math.min(remaining * speed, Math.abs(to - from)),
|
|
180
|
+
facing,
|
|
181
|
+
};
|
|
182
|
+
}
|
|
183
|
+
remaining -= ticks;
|
|
184
|
+
}
|
|
185
|
+
return undefined;
|
|
186
|
+
}
|
|
187
|
+
const pathTicks = (stops, speed) => stops
|
|
188
|
+
.slice(1)
|
|
189
|
+
.reduce((total, stop, index) => total + Math.ceil(Math.abs(stop - (stops[index] ?? 0)) / speed), 0);
|
|
190
|
+
const pause = 24;
|
|
191
|
+
// Idle: pause, walk to 1 edge, pause, walk to the other, pause, walk home.
|
|
192
|
+
function wanderAt(t, width, anchor) {
|
|
193
|
+
const { minimum, maximum } = marginOf(width);
|
|
194
|
+
const legs = [
|
|
195
|
+
[anchor, maximum],
|
|
196
|
+
[maximum, minimum],
|
|
197
|
+
[minimum, anchor],
|
|
198
|
+
];
|
|
199
|
+
let remaining = t;
|
|
200
|
+
for (const [from, to] of legs) {
|
|
201
|
+
if (remaining < pause) {
|
|
202
|
+
return { x: from, facing: 1, resting: remaining };
|
|
203
|
+
}
|
|
204
|
+
remaining -= pause;
|
|
205
|
+
const walked = along([from, to], 1, remaining);
|
|
206
|
+
if (walked) {
|
|
207
|
+
return { ...walked, walking: remaining };
|
|
50
208
|
}
|
|
209
|
+
remaining -= Math.abs(to - from);
|
|
51
210
|
}
|
|
211
|
+
return { x: anchor, facing: 1, resting: 0 };
|
|
212
|
+
}
|
|
213
|
+
const runStops = (width, anchor) => {
|
|
214
|
+
const { minimum, maximum } = marginOf(width);
|
|
215
|
+
return [anchor, maximum, minimum, anchor];
|
|
52
216
|
};
|
|
53
|
-
const
|
|
54
|
-
|
|
55
|
-
|
|
217
|
+
const cycles = {
|
|
218
|
+
focus: 16,
|
|
219
|
+
break: 40,
|
|
220
|
+
eat: 32,
|
|
221
|
+
lift: 32,
|
|
222
|
+
stretch: 40,
|
|
223
|
+
levelUp: 16,
|
|
224
|
+
};
|
|
225
|
+
// How many ticks one loop of an activity takes.
|
|
226
|
+
export function cycleOf(activity, width, anchor = homeOf(width), mood = "content") {
|
|
227
|
+
if (activity === "idle") {
|
|
228
|
+
const { minimum, maximum } = marginOf(width);
|
|
229
|
+
return mood === "sleepy" ? 48 : 3 * pause + 2 * (maximum - minimum);
|
|
230
|
+
}
|
|
231
|
+
if (activity === "run") {
|
|
232
|
+
return pathTicks(runStops(width, anchor), 2);
|
|
56
233
|
}
|
|
234
|
+
return cycles[activity];
|
|
235
|
+
}
|
|
236
|
+
const smileOf = (mood) => (mood === "happy" ? "smile" : undefined);
|
|
237
|
+
// A standing desk at the waist, with the laptop's lid facing out.
|
|
238
|
+
const desk = (put, _upper, b) => {
|
|
239
|
+
const top = b.hip + 1;
|
|
240
|
+
span(put, -9, 9, top, "L");
|
|
241
|
+
span(put, -9, 9, top - 1, "x");
|
|
242
|
+
for (let y = 0; y < top - 1; y++) {
|
|
243
|
+
put(-8, y, "x");
|
|
244
|
+
put(8, y, "x");
|
|
245
|
+
}
|
|
246
|
+
for (let y = top + 1; y <= top + 4; y++) {
|
|
247
|
+
span(put, -3, 3, y, "d");
|
|
248
|
+
}
|
|
249
|
+
span(put, -3, 3, top + 1, "D");
|
|
250
|
+
put(0, top + 3, "l");
|
|
57
251
|
};
|
|
58
|
-
const
|
|
59
|
-
span(
|
|
60
|
-
|
|
61
|
-
|
|
252
|
+
const mug = ([x, y], steam) => (_put, upper) => {
|
|
253
|
+
span(upper, x, x + 2, y, "Q");
|
|
254
|
+
span(upper, x, x + 2, y + 1, "Q");
|
|
255
|
+
span(upper, x, x + 2, y + 2, "U");
|
|
256
|
+
upper(x + 1, y + 2, "o");
|
|
257
|
+
upper(x + 3, y + 1, "Q");
|
|
258
|
+
if (steam !== undefined) {
|
|
259
|
+
const sway = Math.floor(steam / 3) % 2;
|
|
260
|
+
upper(x + sway, y + 4, "v");
|
|
261
|
+
upper(x + 1 - sway, y + 5, "v");
|
|
262
|
+
upper(x + 2 - sway, y + 6, "v");
|
|
263
|
+
}
|
|
264
|
+
};
|
|
265
|
+
const apple = ([x, y], isBitten) => (_put, upper) => {
|
|
266
|
+
span(upper, x, x + 2, y, "F");
|
|
267
|
+
span(upper, x, x + 2, y + 1, "F");
|
|
268
|
+
span(upper, x, x + 1, y + 2, "F");
|
|
269
|
+
upper(x + 2, y + 2, isBitten ? "w" : "F");
|
|
270
|
+
upper(x, y + 1, "T");
|
|
271
|
+
upper(x + 1, y + 3, "G");
|
|
272
|
+
upper(x + 2, y + 4, "G");
|
|
273
|
+
};
|
|
274
|
+
const barbell = (y) => (_put, upper, b) => {
|
|
275
|
+
const end = b.chest + 7;
|
|
276
|
+
span(upper, -end, end, y, "d");
|
|
277
|
+
for (const x of [-end, -end + 1, end - 1, end]) {
|
|
278
|
+
for (let dy = -2; dy <= 2; dy++) {
|
|
279
|
+
upper(x, y + dy, Math.abs(x) === end ? "D" : "d");
|
|
280
|
+
}
|
|
62
281
|
}
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
282
|
+
};
|
|
283
|
+
const sparkles = (t) => (put, _upper, b) => {
|
|
284
|
+
const points = t % 4 < 2
|
|
285
|
+
? [
|
|
286
|
+
[-9, b.top],
|
|
287
|
+
[9, b.top + 3],
|
|
288
|
+
[-11, b.shoulder],
|
|
289
|
+
[11, b.shoulder - 3],
|
|
290
|
+
[0, b.top + 5],
|
|
291
|
+
]
|
|
292
|
+
: [
|
|
293
|
+
[-10, b.top + 3],
|
|
294
|
+
[10, b.top],
|
|
295
|
+
[-9, b.shoulder - 3],
|
|
296
|
+
[9, b.shoulder],
|
|
297
|
+
[-3, b.top + 4],
|
|
298
|
+
];
|
|
299
|
+
for (const [x, y] of points) {
|
|
300
|
+
put(x, y, "y");
|
|
301
|
+
put(x - 1, y, "y");
|
|
302
|
+
put(x + 1, y, "y");
|
|
303
|
+
put(x, y - 1, "y");
|
|
304
|
+
put(x, y + 1, "y");
|
|
305
|
+
}
|
|
306
|
+
};
|
|
307
|
+
const sleepyZ = (t) => (put, _upper, b) => {
|
|
308
|
+
const rise = Math.floor((t % 24) / 3);
|
|
309
|
+
const rows = ["ZZZZ", "..Z.", ".Z..", "ZZZZ"];
|
|
310
|
+
for (const [offset, row] of rows.entries()) {
|
|
311
|
+
for (const [index, key] of [...row].entries()) {
|
|
312
|
+
put(4 + (rise % 2) + index, b.top + 1 + rise - offset, key);
|
|
313
|
+
}
|
|
67
314
|
}
|
|
68
315
|
};
|
|
69
|
-
|
|
70
|
-
|
|
316
|
+
// Standing still between walks: breathing, blinking, looking about, shaking
|
|
317
|
+
// out the hands, and waving when every target is met.
|
|
318
|
+
const resting = (t, mood) => (b) => {
|
|
319
|
+
let turn = 0;
|
|
320
|
+
if (t >= 8 && t < 12) {
|
|
321
|
+
turn = -1;
|
|
322
|
+
}
|
|
323
|
+
else if (t >= 12 && t < 16) {
|
|
324
|
+
turn = 1;
|
|
325
|
+
}
|
|
326
|
+
const isWaving = mood === "happy" && t >= 2 && t < 14;
|
|
327
|
+
const isShaking = t >= 16 && t < 22;
|
|
328
|
+
const shake = isShaking ? t % 2 : 0;
|
|
329
|
+
let hands = [
|
|
330
|
+
hanging(b, -1, shake),
|
|
331
|
+
hanging(b, 1, isShaking ? 1 - shake : 0),
|
|
332
|
+
];
|
|
333
|
+
if (isWaving) {
|
|
334
|
+
hands = [
|
|
335
|
+
hanging(b, -1),
|
|
336
|
+
[b.chest + 3 + (Math.floor(t / 2) % 2), b.top - 1],
|
|
337
|
+
];
|
|
338
|
+
}
|
|
339
|
+
return {
|
|
340
|
+
view: "front",
|
|
341
|
+
mouth: smileOf(mood),
|
|
342
|
+
bob: breathing(t),
|
|
343
|
+
eyes: t === 5 || t === 6 ? "closed" : "open",
|
|
344
|
+
turn,
|
|
345
|
+
hands,
|
|
346
|
+
};
|
|
71
347
|
};
|
|
72
|
-
// Where the
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
const
|
|
76
|
-
const
|
|
77
|
-
const
|
|
78
|
-
const
|
|
348
|
+
// Where the character stands and what it does on tick `t` of an activity
|
|
349
|
+
// that began at `anchor`. Every loop starts and ends at the anchor.
|
|
350
|
+
function sceneOf(activity, t, width, mood, anchor) {
|
|
351
|
+
const cycle = cycleOf(activity, width, anchor, mood);
|
|
352
|
+
const at = t % cycle;
|
|
353
|
+
const mouth = smileOf(mood);
|
|
354
|
+
const still = (pose, props, lift = 0) => ({
|
|
355
|
+
x: anchor,
|
|
356
|
+
facing: 1,
|
|
357
|
+
lift,
|
|
358
|
+
pose,
|
|
359
|
+
props,
|
|
360
|
+
});
|
|
79
361
|
if (activity === "idle" && mood === "sleepy") {
|
|
80
|
-
//
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
],
|
|
91
|
-
};
|
|
362
|
+
// Dozing on its feet: nodding off, yawning, with Zs drifting up.
|
|
363
|
+
const isYawning = at >= 36 && at < 44;
|
|
364
|
+
return still((b) => ({
|
|
365
|
+
view: "front",
|
|
366
|
+
eyes: "closed",
|
|
367
|
+
nod: at % 16 >= 8 ? 1 : 0,
|
|
368
|
+
bob: breathing(at, 8),
|
|
369
|
+
mouth: isYawning ? "open" : undefined,
|
|
370
|
+
hands: isYawning ? [undefined, [2, b.chin]] : undefined,
|
|
371
|
+
}), [sleepyZ(at)]);
|
|
92
372
|
}
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
});
|
|
110
|
-
if (at < pause) {
|
|
111
|
-
return {
|
|
112
|
-
x: minimum,
|
|
113
|
-
facing: at === 2 || at === 3 ? -1 : 1,
|
|
114
|
-
pose: rest(at),
|
|
115
|
-
};
|
|
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;
|
|
373
|
+
switch (activity) {
|
|
374
|
+
case "idle": {
|
|
375
|
+
const place = wanderAt(at, width, anchor);
|
|
376
|
+
if ("walking" in place) {
|
|
377
|
+
return {
|
|
378
|
+
x: place.x,
|
|
379
|
+
facing: place.facing,
|
|
380
|
+
lift: 0,
|
|
381
|
+
pose: () => ({
|
|
382
|
+
view: "side",
|
|
383
|
+
frame: place.walking,
|
|
384
|
+
gait: "walk",
|
|
385
|
+
mouth,
|
|
386
|
+
}),
|
|
387
|
+
};
|
|
388
|
+
}
|
|
122
389
|
return {
|
|
123
|
-
x:
|
|
124
|
-
facing:
|
|
125
|
-
|
|
390
|
+
x: place.x,
|
|
391
|
+
facing: 1,
|
|
392
|
+
lift: mood === "happy" && place.resting === 1 ? 1 : 0,
|
|
393
|
+
pose: resting(place.resting, mood),
|
|
126
394
|
};
|
|
127
395
|
}
|
|
128
|
-
const step = at - 2 * pause - travel;
|
|
129
|
-
return { x: maximum - step, facing: -1, pose: walk(step) };
|
|
130
|
-
}
|
|
131
|
-
switch (activity) {
|
|
132
396
|
case "run": {
|
|
133
|
-
|
|
134
|
-
const
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
return {
|
|
138
|
-
x: forward
|
|
139
|
-
? Math.min(minimum + step * 2, maximum)
|
|
140
|
-
: Math.max(maximum - step * 2, minimum),
|
|
141
|
-
facing: forward ? 1 : -1,
|
|
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)],
|
|
397
|
+
// A lap out to 1 edge, across to the other, and back home.
|
|
398
|
+
const place = along(runStops(width, anchor), 2, at) ?? {
|
|
399
|
+
x: anchor,
|
|
400
|
+
facing: 1,
|
|
149
401
|
};
|
|
150
|
-
}
|
|
151
|
-
case "focus": {
|
|
152
402
|
return {
|
|
153
|
-
x:
|
|
154
|
-
facing:
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
squash: tick < 2 ? 0 : 1,
|
|
158
|
-
arms: ["down", tick % 2 === 0 ? "forward" : "mid"],
|
|
159
|
-
},
|
|
403
|
+
x: place.x,
|
|
404
|
+
facing: place.facing,
|
|
405
|
+
lift: 0,
|
|
406
|
+
pose: () => ({ view: "side", frame: at, gait: "run", mouth }),
|
|
160
407
|
props: [
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
put(
|
|
164
|
-
put(body + 7 - (tick % 2), 9 + ((tick + 2) % 4), "y");
|
|
408
|
+
(put, _upper, b) => {
|
|
409
|
+
put(-(b.chest + 3), b.top - 1 - (at % 2), "v");
|
|
410
|
+
put(-(b.chest + 5), b.hip + (at % 3), "v");
|
|
165
411
|
},
|
|
166
412
|
],
|
|
167
413
|
};
|
|
168
414
|
}
|
|
415
|
+
case "focus": {
|
|
416
|
+
// Typing at a standing desk, the screen glowing.
|
|
417
|
+
const key = Math.floor(at / 2) % 2;
|
|
418
|
+
return still((b) => ({
|
|
419
|
+
view: "front",
|
|
420
|
+
mouth,
|
|
421
|
+
bob: breathing(at, 8),
|
|
422
|
+
hands: [
|
|
423
|
+
[-3, b.hip + 3 + key],
|
|
424
|
+
[3, b.hip + 4 - key],
|
|
425
|
+
],
|
|
426
|
+
}), [
|
|
427
|
+
desk,
|
|
428
|
+
(put, _upper, b) => {
|
|
429
|
+
put(-2 + (at % 5), b.top + 2 + key, "y");
|
|
430
|
+
},
|
|
431
|
+
]);
|
|
432
|
+
}
|
|
169
433
|
case "break": {
|
|
170
|
-
|
|
171
|
-
const
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
434
|
+
// A coffee, sipped every few seconds.
|
|
435
|
+
const isSipping = at >= 28;
|
|
436
|
+
const hand = (b) => isSipping ? [2, b.chin] : [b.chest + 2, b.hip + 2];
|
|
437
|
+
return still((b) => ({
|
|
438
|
+
view: "front",
|
|
439
|
+
mouth,
|
|
440
|
+
bob: breathing(at, isSipping ? 2 : 6),
|
|
441
|
+
eyes: isSipping ? "closed" : "open",
|
|
442
|
+
hands: [undefined, hand(b)],
|
|
443
|
+
}), [
|
|
444
|
+
(put, upper, b) => {
|
|
445
|
+
const [x, y] = hand(b);
|
|
446
|
+
mug([x, y + 1], isSipping ? undefined : at)(put, upper, b);
|
|
180
447
|
},
|
|
181
|
-
|
|
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
|
-
]),
|
|
188
|
-
],
|
|
189
|
-
};
|
|
448
|
+
]);
|
|
190
449
|
}
|
|
191
450
|
case "eat": {
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
451
|
+
// Raise the apple, take a bite, chew, and lower it.
|
|
452
|
+
const isRaised = at >= 12 && at < 24;
|
|
453
|
+
const isChewing = at >= 16 && at < 24;
|
|
454
|
+
const hand = (b) => isRaised ? [2, b.chin - 1] : [b.chest + 2, b.hip + 3];
|
|
455
|
+
return still((b) => ({
|
|
456
|
+
view: "front",
|
|
457
|
+
bob: breathing(at, 4),
|
|
458
|
+
mouth: isChewing && at % 2 === 0 ? "open" : mouth,
|
|
459
|
+
hands: [undefined, hand(b)],
|
|
460
|
+
}), [
|
|
461
|
+
(put, upper, b) => {
|
|
462
|
+
const [x, y] = hand(b);
|
|
463
|
+
apple([x, y], at >= 18)(put, upper, b);
|
|
201
464
|
},
|
|
202
|
-
|
|
203
|
-
held(tick === 3 ? [".G", "F.", "FF"] : [".G", "FF", "FF"], (shape) => atMouth ? shape.top - 2 : Math.ceil(shape.height / 2) + 2),
|
|
204
|
-
],
|
|
205
|
-
};
|
|
465
|
+
]);
|
|
206
466
|
}
|
|
207
467
|
case "lift": {
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
icon(["DD", "DD", "DD"], shape.width + 2, y + 1)(put, shape, 0);
|
|
223
|
-
},
|
|
224
|
-
],
|
|
468
|
+
// An overhead press: rack, press, lock out, lower, rest.
|
|
469
|
+
const height = (b) => {
|
|
470
|
+
const rack = b.shoulder;
|
|
471
|
+
const lockout = b.top + 2;
|
|
472
|
+
if (at < 8 || at >= 28) {
|
|
473
|
+
return rack;
|
|
474
|
+
}
|
|
475
|
+
if (at < 12) {
|
|
476
|
+
return rack + Math.round(((lockout - rack) * (at - 7)) / 4);
|
|
477
|
+
}
|
|
478
|
+
if (at < 20) {
|
|
479
|
+
return lockout;
|
|
480
|
+
}
|
|
481
|
+
return lockout - Math.round(((lockout - rack) * (at - 19)) / 8);
|
|
225
482
|
};
|
|
483
|
+
return still((b) => {
|
|
484
|
+
const y = height(b);
|
|
485
|
+
const isLocked = at >= 12 && at < 20;
|
|
486
|
+
const x = isLocked ? b.chest + 1 : b.chest + 3;
|
|
487
|
+
return {
|
|
488
|
+
view: "front",
|
|
489
|
+
mouth,
|
|
490
|
+
bob: breathing(at, isLocked ? 2 : 4),
|
|
491
|
+
hands: [
|
|
492
|
+
[-x, y],
|
|
493
|
+
[x, y],
|
|
494
|
+
],
|
|
495
|
+
};
|
|
496
|
+
}, [
|
|
497
|
+
(put, upper, b) => {
|
|
498
|
+
barbell(height(b) + 1)(put, upper, b);
|
|
499
|
+
},
|
|
500
|
+
]);
|
|
226
501
|
}
|
|
227
502
|
case "stretch": {
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
503
|
+
// Reach up, rise on the toes, open the arms wide, shake them out.
|
|
504
|
+
const phase = Math.floor(at / 10);
|
|
505
|
+
return still((b) => {
|
|
506
|
+
const reaches = [
|
|
507
|
+
[1, b.top + 3],
|
|
508
|
+
[1, b.top + 4],
|
|
509
|
+
[b.chest + 8, b.shoulder + 1],
|
|
510
|
+
[b.chest + 1, b.shoulder - b.reach + 1 + (at % 2)],
|
|
511
|
+
];
|
|
512
|
+
const [x, y] = reaches[phase] ?? [1, b.top + 3];
|
|
513
|
+
return {
|
|
514
|
+
view: "front",
|
|
515
|
+
mouth,
|
|
516
|
+
bob: breathing(at, 5),
|
|
517
|
+
eyes: phase === 1 ? "closed" : "open",
|
|
518
|
+
hands: [
|
|
519
|
+
[-x, phase === 3 ? y + 1 - (at % 2) : y],
|
|
520
|
+
[x, y],
|
|
521
|
+
],
|
|
522
|
+
};
|
|
523
|
+
}, undefined, phase === 1 ? 1 : 0);
|
|
237
524
|
}
|
|
238
525
|
case "levelUp": {
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
sparkles(tick % 2 === 0
|
|
249
|
-
? [
|
|
250
|
-
[-3, 2],
|
|
251
|
-
[3, 4],
|
|
252
|
-
[-4, -3],
|
|
253
|
-
[4, -1],
|
|
254
|
-
]
|
|
255
|
-
: [
|
|
256
|
-
[-4, 4],
|
|
257
|
-
[4, 2],
|
|
258
|
-
[-3, -1],
|
|
259
|
-
[3, -3],
|
|
260
|
-
]),
|
|
526
|
+
// Crouch, jump with both arms up, land, with sparkles.
|
|
527
|
+
const lift = [0, 0, 0, 1, 2, 3, 4, 4, 3, 2, 1, 0, 0, 0, 0, 0][at] ?? 0;
|
|
528
|
+
return still((b) => ({
|
|
529
|
+
view: "front",
|
|
530
|
+
mouth: "smile",
|
|
531
|
+
bob: at < 3 || at > 11 ? 1 : 0,
|
|
532
|
+
hands: [
|
|
533
|
+
[-(b.chest + 4), b.top + 1],
|
|
534
|
+
[b.chest + 4, b.top + 1],
|
|
261
535
|
],
|
|
262
|
-
};
|
|
536
|
+
}), [sparkles(at)], lift);
|
|
263
537
|
}
|
|
264
538
|
}
|
|
265
539
|
}
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
return 2 * Math.max(Math.ceil(travel / 2), 1);
|
|
540
|
+
// Where the character is on tick `t` of an activity, to keep moving from
|
|
541
|
+
// there when the next one begins.
|
|
542
|
+
export function placeOf(activity, t, width, mood, anchor) {
|
|
543
|
+
const scene = sceneOf(activity, t, width, mood, anchor);
|
|
544
|
+
return { x: scene.x, lift: scene.lift };
|
|
545
|
+
}
|
|
546
|
+
export const startMotion = (activity, mood, width) => ({ activity, mood, anchor: homeOf(width), tick: 0 });
|
|
547
|
+
// 1 frame later. A new activity starts where the character stands: idling
|
|
548
|
+
// gives way at once, and anything else first finishes its loop back home, so
|
|
549
|
+
// the character never jumps.
|
|
550
|
+
export function advance(motion, wanted, width) {
|
|
551
|
+
const isSame = wanted.activity === motion.activity && wanted.mood === motion.mood;
|
|
552
|
+
if (!isSame) {
|
|
553
|
+
const { x, lift } = placeOf(motion.activity, motion.tick, width, motion.mood, motion.anchor);
|
|
554
|
+
if (motion.activity === "idle" || (x === motion.anchor && lift === 0)) {
|
|
555
|
+
return { ...wanted, anchor: x, tick: 0 };
|
|
556
|
+
}
|
|
284
557
|
}
|
|
285
|
-
return
|
|
558
|
+
return { ...motion, tick: motion.tick + 1 };
|
|
286
559
|
}
|
|
287
|
-
//
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
560
|
+
// The tops by work style: a hoodie, a shirt under an apron, a tee, a tank.
|
|
561
|
+
const topOf = {
|
|
562
|
+
desk: { main: "C", shade: "c", light: "E" },
|
|
563
|
+
standing: { main: "Q", shade: "U", light: "w" },
|
|
564
|
+
active: { main: "C", shade: "c", light: "E" },
|
|
565
|
+
athlete: { main: "S", shade: "z", light: "T" },
|
|
566
|
+
};
|
|
567
|
+
// How much of the arm the sleeve covers, from the shoulder down.
|
|
568
|
+
const sleeveOf = {
|
|
569
|
+
desk: 1,
|
|
570
|
+
standing: 0.5,
|
|
571
|
+
active: 0.35,
|
|
572
|
+
athlete: 0,
|
|
573
|
+
};
|
|
574
|
+
// Jeans and sneakers, chinos and shoes, cargo pants and boots, or shorts
|
|
575
|
+
// and running shoes.
|
|
576
|
+
const bottomOf = {
|
|
577
|
+
desk: {
|
|
578
|
+
main: "P",
|
|
579
|
+
shade: "p",
|
|
580
|
+
light: "R",
|
|
581
|
+
shoe: "w",
|
|
582
|
+
sole: "Y",
|
|
583
|
+
shoeRows: 2,
|
|
584
|
+
},
|
|
585
|
+
standing: {
|
|
586
|
+
main: "N",
|
|
587
|
+
shade: "n",
|
|
588
|
+
light: "N",
|
|
589
|
+
shoe: "b",
|
|
590
|
+
sole: "o",
|
|
591
|
+
shoeRows: 2,
|
|
592
|
+
},
|
|
593
|
+
active: {
|
|
594
|
+
main: "O",
|
|
595
|
+
shade: "V",
|
|
596
|
+
light: "O",
|
|
597
|
+
shoe: "B",
|
|
598
|
+
sole: "e",
|
|
599
|
+
shoeRows: 3,
|
|
600
|
+
},
|
|
601
|
+
athlete: {
|
|
602
|
+
main: "x",
|
|
603
|
+
shade: "x",
|
|
604
|
+
light: "x",
|
|
605
|
+
shoe: "S",
|
|
606
|
+
sole: "w",
|
|
607
|
+
shoeRows: 2,
|
|
608
|
+
},
|
|
609
|
+
};
|
|
610
|
+
const hairOf = {
|
|
611
|
+
dark: ["h", "i"],
|
|
612
|
+
grey: ["H", "J"],
|
|
613
|
+
white: ["I", "K"],
|
|
614
|
+
};
|
|
615
|
+
// An oval on the ground with the feet in its middle, shrinking in the air.
|
|
616
|
+
function paintShadow(put, b, lift) {
|
|
617
|
+
const half = b.hips + 5 - Math.min(lift, 3);
|
|
618
|
+
span(put, -half + 3, half - 3, ground - 2, "A");
|
|
619
|
+
span(put, -half + 1, half - 1, ground - 1, "A");
|
|
620
|
+
span(put, -half, half, ground, "A");
|
|
621
|
+
span(put, -half + 2, half - 2, ground + 1, "A");
|
|
622
|
+
}
|
|
623
|
+
// Long hair falls behind the shoulders, so it goes on before the body.
|
|
624
|
+
function paintHairBehind({ upper, b, look, counter }, nod) {
|
|
625
|
+
const [hair, light] = hairOf[look.hair];
|
|
626
|
+
const head = (x, y, key) => {
|
|
627
|
+
upper(x, y - nod, key);
|
|
628
|
+
};
|
|
629
|
+
if (look.gender === "other") {
|
|
630
|
+
for (let y = b.chin + 1; y <= b.chin + 4; y++) {
|
|
631
|
+
head(-4, y, hair);
|
|
632
|
+
head(4, y, hair);
|
|
293
633
|
}
|
|
634
|
+
return;
|
|
294
635
|
}
|
|
295
|
-
if (
|
|
296
|
-
|
|
297
|
-
put(-3 - sway, shape.top - 1, "y");
|
|
298
|
-
put(shape.width + 2 + sway, shape.top - 3, "y");
|
|
299
|
-
put(-2, 3 + sway, "y");
|
|
636
|
+
if (look.gender !== "female") {
|
|
637
|
+
return;
|
|
300
638
|
}
|
|
301
|
-
|
|
302
|
-
//
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
if (!isLifted) {
|
|
310
|
-
put(x, 0, foot);
|
|
311
|
-
}
|
|
312
|
-
put(x, 1, "o");
|
|
639
|
+
if (look.style === "athlete") {
|
|
640
|
+
// A ponytail that swings.
|
|
641
|
+
const sway = Math.floor(counter / 3) % 2;
|
|
642
|
+
head(4, b.chin + 4, hair);
|
|
643
|
+
head(5, b.chin + 3, hair);
|
|
644
|
+
head(5 + sway, b.chin + 2, light);
|
|
645
|
+
head(5 + sway, b.chin + 1, hair);
|
|
646
|
+
return;
|
|
313
647
|
}
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
span(
|
|
648
|
+
for (let y = b.shoulder - 3; y <= b.chin + 4; y++) {
|
|
649
|
+
head(-4, y, hair);
|
|
650
|
+
head(4, y, y % 3 === 0 ? light : hair);
|
|
651
|
+
}
|
|
652
|
+
for (let y = b.shoulder - 2; y < b.chin; y++) {
|
|
653
|
+
span(upper, -3, 3, y, hair);
|
|
320
654
|
}
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
655
|
+
}
|
|
656
|
+
function legKeyOf(look, b, at, side) {
|
|
657
|
+
const bottom = bottomOf[look.style];
|
|
658
|
+
const shorts = ground + Math.round(b.legs * 0.55);
|
|
659
|
+
if (at.y < at.sole + bottom.shoeRows) {
|
|
660
|
+
return at.y === at.sole ? bottom.sole : bottom.shoe;
|
|
324
661
|
}
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
icon(["r.r", "rrr", ".r."], middle - 1, 6)(put, shape, 0);
|
|
662
|
+
if (look.style === "athlete" && at.y < shorts) {
|
|
663
|
+
return at.y === b.knee - 1 && at.x === 2 ? "q" : "s";
|
|
328
664
|
}
|
|
329
|
-
if (
|
|
330
|
-
|
|
331
|
-
put(-1, top - 5, "S");
|
|
332
|
-
put(-2, top - 6, "s");
|
|
665
|
+
if (at.x === at.width) {
|
|
666
|
+
return side < 0 ? bottom.light : bottom.shade;
|
|
333
667
|
}
|
|
668
|
+
return at.y === at.sole + bottom.shoeRows ? bottom.shade : bottom.main;
|
|
334
669
|
}
|
|
335
|
-
function
|
|
336
|
-
const
|
|
337
|
-
const
|
|
338
|
-
const
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
670
|
+
function paintLegsFront({ put, b, look }, pose) {
|
|
671
|
+
const bottom = bottomOf[look.style];
|
|
672
|
+
const shorts = ground + Math.round(b.legs * 0.55);
|
|
673
|
+
for (const side of [-1, 1]) {
|
|
674
|
+
const isLifted = (side === -1 && pose.step === 1) || (side === 1 && pose.step === 3);
|
|
675
|
+
const sole = ground + (isLifted ? 1 : 0);
|
|
676
|
+
for (let y = sole; y < b.hip; y++) {
|
|
677
|
+
let width = b.ankle;
|
|
678
|
+
if (y < sole + bottom.shoeRows) {
|
|
679
|
+
width = b.ankle + 1;
|
|
680
|
+
}
|
|
681
|
+
else if (y >= b.knee) {
|
|
682
|
+
width = b.thigh;
|
|
683
|
+
}
|
|
684
|
+
else if (y >= b.shin) {
|
|
685
|
+
width = b.calf;
|
|
686
|
+
}
|
|
687
|
+
for (let x = 1; x <= width; x++) {
|
|
688
|
+
put(side * x, y, legKeyOf(look, b, { x, y, sole, width }, side));
|
|
689
|
+
}
|
|
345
690
|
}
|
|
346
|
-
|
|
347
|
-
put(
|
|
348
|
-
put(
|
|
349
|
-
put(
|
|
350
|
-
put(x, eyeY - 2, "r");
|
|
691
|
+
if (look.style === "active") {
|
|
692
|
+
put(side * b.thigh, b.knee + 1, "V");
|
|
693
|
+
put(side * b.thigh, b.knee + 2, "V");
|
|
694
|
+
put(side * (b.thigh - 1), b.knee + 2, "V");
|
|
351
695
|
}
|
|
352
|
-
|
|
353
|
-
put(
|
|
354
|
-
put(x, eyeY + 1, counter % 23 === 0 ? "O" : "e");
|
|
696
|
+
if (look.style === "athlete") {
|
|
697
|
+
put(side * b.thigh, shorts + 1, "S");
|
|
355
698
|
}
|
|
356
699
|
}
|
|
700
|
+
// The thighs meet at the crotch, and all the way down on a heavy build.
|
|
701
|
+
let crotch = b.hip - 2;
|
|
702
|
+
if (look.build === "heavy") {
|
|
703
|
+
crotch = look.style === "athlete" ? shorts : b.knee;
|
|
704
|
+
}
|
|
705
|
+
for (let y = crotch; y < b.hip; y++) {
|
|
706
|
+
put(0, y, bottom.main);
|
|
707
|
+
}
|
|
357
708
|
}
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
const
|
|
361
|
-
const
|
|
362
|
-
const
|
|
363
|
-
|
|
364
|
-
|
|
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");
|
|
709
|
+
function paintTorsoFront({ upper, b, look, gear }) {
|
|
710
|
+
const top = topOf[look.style];
|
|
711
|
+
const torso = b.shoulder - b.hip + 1;
|
|
712
|
+
const chestRows = Math.ceil(torso * 0.45);
|
|
713
|
+
const halfAt = (y) => {
|
|
714
|
+
if (b.shoulder - y < chestRows) {
|
|
715
|
+
return b.chest;
|
|
372
716
|
}
|
|
717
|
+
return y - b.hip < 2 ? Math.max(b.hips, b.waist) : b.waist;
|
|
373
718
|
};
|
|
374
|
-
|
|
375
|
-
|
|
719
|
+
for (let y = b.hip; y < b.shoulder; y++) {
|
|
720
|
+
const half = halfAt(y);
|
|
721
|
+
span(upper, -half, half, y, top.main);
|
|
722
|
+
upper(-half, y, top.light);
|
|
723
|
+
upper(half, y, top.shade);
|
|
724
|
+
}
|
|
725
|
+
// The shoulder line runs out over the tops of the arms.
|
|
726
|
+
const shoulderKey = look.style === "athlete" ? "s" : top.main;
|
|
727
|
+
span(upper, -(b.chest + 1), b.chest + 1, b.shoulder, shoulderKey);
|
|
728
|
+
span(upper, -(b.chest - 2), b.chest - 2, b.shoulder, top.main);
|
|
729
|
+
if (look.build === "heavy") {
|
|
730
|
+
// The belly's curve.
|
|
731
|
+
span(upper, -(b.waist - 1), b.waist - 1, b.hip + 1, top.shade);
|
|
732
|
+
}
|
|
733
|
+
// The neck, shadowed under the chin.
|
|
734
|
+
span(upper, -1, 1, b.shoulder + 1, "s");
|
|
735
|
+
upper(1, b.shoulder + 1, "q");
|
|
736
|
+
switch (look.style) {
|
|
737
|
+
case "desk": {
|
|
738
|
+
// A hoodie: the hood around the neck, drawstrings, a front pocket.
|
|
739
|
+
span(upper, -3, -2, b.shoulder + 1, top.shade);
|
|
740
|
+
span(upper, 2, 3, b.shoulder + 1, top.shade);
|
|
741
|
+
span(upper, -2, 2, b.shoulder, top.shade);
|
|
742
|
+
for (const x of [-1, 1]) {
|
|
743
|
+
upper(x, b.shoulder - 1, "w");
|
|
744
|
+
upper(x, b.shoulder - 2, "w");
|
|
745
|
+
}
|
|
746
|
+
span(upper, -3, 3, b.hip + 2, top.shade);
|
|
747
|
+
span(upper, -3, 3, b.hip + 1, top.main);
|
|
748
|
+
upper(-3, b.hip + 1, top.shade);
|
|
749
|
+
upper(3, b.hip + 1, top.shade);
|
|
750
|
+
span(upper, -(b.waist - 1), b.waist - 1, b.hip, top.shade);
|
|
751
|
+
break;
|
|
752
|
+
}
|
|
753
|
+
case "standing": {
|
|
754
|
+
// A collared shirt under an apron that hangs over the thighs.
|
|
755
|
+
span(upper, -2, 2, b.shoulder, "w");
|
|
756
|
+
upper(0, b.shoulder, "s");
|
|
757
|
+
for (const x of [-2, 2]) {
|
|
758
|
+
upper(x, b.shoulder - 1, "S");
|
|
759
|
+
upper(x, b.shoulder - 2, "S");
|
|
760
|
+
}
|
|
761
|
+
for (let y = b.hip - 3; y <= b.shoulder - 3; y++) {
|
|
762
|
+
span(upper, -b.waist, b.waist, y, "S");
|
|
763
|
+
upper(b.waist, y, "z");
|
|
764
|
+
upper(-b.waist, y, "T");
|
|
765
|
+
}
|
|
766
|
+
span(upper, -2, 2, b.hip + 1, "z");
|
|
767
|
+
span(upper, -1, 1, b.hip + 2, "S");
|
|
768
|
+
upper(-(b.waist + 1), b.hip + 2, "S");
|
|
769
|
+
upper(b.waist + 1, b.hip + 2, "S");
|
|
770
|
+
break;
|
|
771
|
+
}
|
|
772
|
+
case "active": {
|
|
773
|
+
// A crew-neck tee with a chest pocket.
|
|
774
|
+
span(upper, -1, 1, b.shoulder, top.shade);
|
|
775
|
+
upper(-2, b.shoulder - 2, top.shade);
|
|
776
|
+
upper(-3, b.shoulder - 2, top.shade);
|
|
777
|
+
break;
|
|
778
|
+
}
|
|
779
|
+
case "athlete": {
|
|
780
|
+
// A racerback tank with a stripe; abs show on a lean build.
|
|
781
|
+
span(upper, -1, 1, b.shoulder, "s");
|
|
782
|
+
span(upper, -b.chest, b.chest, b.shoulder - 3, top.shade);
|
|
783
|
+
if (look.build === "athletic") {
|
|
784
|
+
for (let y = b.hip + 2; y < b.shoulder - 4; y += 2) {
|
|
785
|
+
upper(-1, y, top.shade);
|
|
786
|
+
upper(1, y, top.shade);
|
|
787
|
+
}
|
|
788
|
+
}
|
|
789
|
+
break;
|
|
790
|
+
}
|
|
791
|
+
}
|
|
792
|
+
if (gear.wear === 2) {
|
|
793
|
+
// An open jacket over whatever is underneath.
|
|
794
|
+
for (let y = b.hip; y <= b.shoulder; y++) {
|
|
795
|
+
const half = y === b.shoulder ? b.chest + 1 : halfAt(y);
|
|
796
|
+
span(upper, -half, -half + 1, y, "M");
|
|
797
|
+
span(upper, half - 1, half, y, "X");
|
|
798
|
+
}
|
|
799
|
+
upper(-2, b.shoulder, "M");
|
|
800
|
+
upper(2, b.shoulder, "X");
|
|
801
|
+
}
|
|
802
|
+
if (gear.carry >= 1) {
|
|
803
|
+
// Backpack straps over both shoulders.
|
|
804
|
+
for (let y = b.shoulder - 4; y <= b.shoulder; y++) {
|
|
805
|
+
upper(-(b.chest - 1), y, "B");
|
|
806
|
+
upper(b.chest - 1, y, "B");
|
|
807
|
+
}
|
|
808
|
+
upper(-(b.chest - 1), b.shoulder - 3, "d");
|
|
809
|
+
upper(b.chest - 1, b.shoulder - 3, "d");
|
|
810
|
+
}
|
|
811
|
+
if (gear.wear >= 1) {
|
|
812
|
+
// A scarf around the neck, its end hanging down the chest.
|
|
813
|
+
span(upper, -2, 2, b.shoulder + 1, "S");
|
|
814
|
+
span(upper, -2, 2, b.shoulder, "z");
|
|
815
|
+
for (let y = b.shoulder - 3; y < b.shoulder; y++) {
|
|
816
|
+
upper(1, y, "S");
|
|
817
|
+
upper(2, y, "z");
|
|
818
|
+
}
|
|
819
|
+
}
|
|
820
|
+
if (gear.carry === 2) {
|
|
821
|
+
// A gold medal on its ribbon.
|
|
822
|
+
upper(-1, b.shoulder - 1, "z");
|
|
823
|
+
upper(1, b.shoulder - 1, "z");
|
|
824
|
+
upper(0, b.shoulder - 2, "z");
|
|
825
|
+
span(upper, -1, 0, b.shoulder - 3, "g");
|
|
826
|
+
span(upper, -1, 0, b.shoulder - 4, "g");
|
|
827
|
+
}
|
|
828
|
+
}
|
|
829
|
+
// An arm from the shoulder to the hand, sleeved by the work style.
|
|
830
|
+
function paintArm({ upper, b, look, gear }, side, hand, depth = "level") {
|
|
831
|
+
const isJacket = gear.wear === 2;
|
|
832
|
+
const tones = isJacket
|
|
833
|
+
? { main: "X", shade: "X", light: "M" }
|
|
834
|
+
: topOf[look.style];
|
|
835
|
+
const sleeve = isJacket ? 1 : sleeveOf[look.style];
|
|
836
|
+
const joint = [side * (b.chest + 1), b.shoulder - 1];
|
|
837
|
+
const points = pointsOf(joint, hand);
|
|
838
|
+
const covered = Math.round((points.length - 1) * sleeve);
|
|
839
|
+
const isLevel = Math.abs(hand[0] - joint[0]) > Math.abs(hand[1] - joint[1]);
|
|
840
|
+
const [dx, dy] = isLevel ? [0, -1] : [side, 0];
|
|
841
|
+
const edgeTone = side < 0 ? tones.light : tones.shade;
|
|
842
|
+
for (const [step, [x, y]] of points.entries()) {
|
|
843
|
+
const isHand = step >= points.length - 2;
|
|
844
|
+
const isSleeve = !isHand && (step < covered || (step === 0 && sleeve > 0));
|
|
845
|
+
let width = Math.max(b.arm - 1, 2);
|
|
846
|
+
if (isHand) {
|
|
847
|
+
width = 2;
|
|
848
|
+
}
|
|
849
|
+
else if (step < points.length / 2) {
|
|
850
|
+
width = b.arm;
|
|
851
|
+
}
|
|
852
|
+
let key = isSleeve ? tones.main : "s";
|
|
853
|
+
let edge = isSleeve ? edgeTone : "q";
|
|
854
|
+
// In profile, the far arm sinks into shadow and the near one catches
|
|
855
|
+
// the light, so both read against the torso.
|
|
856
|
+
if (depth === "far") {
|
|
857
|
+
key = isSleeve ? tones.shade : "q";
|
|
858
|
+
edge = key;
|
|
859
|
+
}
|
|
860
|
+
else if (depth === "near" && isSleeve) {
|
|
861
|
+
key = tones.light;
|
|
862
|
+
}
|
|
863
|
+
for (let index = 0; index < width; index++) {
|
|
864
|
+
upper(x + dx * index, y + dy * index, index === width - 1 ? edge : key);
|
|
865
|
+
}
|
|
866
|
+
}
|
|
867
|
+
const wrist = points.at(-3);
|
|
868
|
+
if (side === 1 && depth !== "far" && gear.gadget === 2 && wrist) {
|
|
869
|
+
upper(wrist[0], wrist[1], "t");
|
|
870
|
+
upper(wrist[0] + dx, wrist[1] + dy, "e");
|
|
871
|
+
}
|
|
376
872
|
}
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
const
|
|
380
|
-
|
|
381
|
-
|
|
873
|
+
function paintHeadFront({ upper, b, look, gear }, pose) {
|
|
874
|
+
const nod = pose.nod ?? 0;
|
|
875
|
+
const head = (x, y, key) => {
|
|
876
|
+
upper(x, y - nod, key);
|
|
877
|
+
};
|
|
878
|
+
const { chin } = b;
|
|
879
|
+
const [hair, light] = hairOf[look.hair];
|
|
880
|
+
const turn = pose.turn ?? 0;
|
|
881
|
+
const jaw = look.build === "heavy" ? 3 : 2;
|
|
882
|
+
span(head, -jaw, jaw, chin, "s");
|
|
883
|
+
head(-jaw, chin, "q");
|
|
884
|
+
head(jaw, chin, "q");
|
|
885
|
+
for (let y = chin + 1; y <= chin + 4; y++) {
|
|
886
|
+
span(head, -3, 3, y, "s");
|
|
887
|
+
}
|
|
888
|
+
head(3, chin + 1, "q");
|
|
889
|
+
head(3, chin + 2, "q");
|
|
890
|
+
span(head, -3, 3, chin + 5, hair);
|
|
891
|
+
span(head, -2, 1, chin + 5, light);
|
|
892
|
+
span(head, -2, 2, chin + 6, hair);
|
|
893
|
+
head(-1, chin + 6, light);
|
|
894
|
+
// Eyes, brows, the shadow under the nose, and the mouth.
|
|
895
|
+
const eye = pose.eyes === "closed" ? "q" : "e";
|
|
896
|
+
head(turn - 2, chin + 3, eye);
|
|
897
|
+
head(turn + 2, chin + 3, eye);
|
|
898
|
+
head(turn - 2, chin + 4, hair);
|
|
899
|
+
head(turn + 2, chin + 4, hair);
|
|
900
|
+
head(turn, chin + 2, "q");
|
|
901
|
+
if (pose.mouth === "smile") {
|
|
902
|
+
span(head, turn - 1, turn + 1, chin + 1, "m");
|
|
903
|
+
head(turn - 2, chin + 2, "r");
|
|
904
|
+
head(turn + 2, chin + 2, "r");
|
|
905
|
+
}
|
|
906
|
+
else if (pose.mouth === "open") {
|
|
907
|
+
span(head, turn - 1, turn, chin + 1, "e");
|
|
908
|
+
}
|
|
909
|
+
else {
|
|
910
|
+
span(head, turn - 1, turn, chin + 1, "q");
|
|
382
911
|
}
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
912
|
+
// Short hair with sideburns and ears, a side-swept fringe, or a bob.
|
|
913
|
+
if (look.gender === "male") {
|
|
914
|
+
head(-3, chin + 4, hair);
|
|
915
|
+
head(3, chin + 4, hair);
|
|
916
|
+
}
|
|
917
|
+
else if (look.gender === "female") {
|
|
918
|
+
span(head, -3, -1, chin + 4, hair);
|
|
919
|
+
head(3, chin + 4, hair);
|
|
920
|
+
head(-3, chin + 3, hair);
|
|
921
|
+
head(3, chin + 3, hair);
|
|
387
922
|
}
|
|
388
923
|
else {
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
924
|
+
span(head, -3, -2, chin + 4, hair);
|
|
925
|
+
head(3, chin + 4, hair);
|
|
926
|
+
}
|
|
927
|
+
if (look.gender === "male" && gear.gadget === 0) {
|
|
928
|
+
head(-4, chin + 2, "q");
|
|
929
|
+
head(-4, chin + 3, "s");
|
|
930
|
+
head(4, chin + 2, "q");
|
|
931
|
+
head(4, chin + 3, "q");
|
|
392
932
|
}
|
|
393
933
|
if (gear.gadget >= 1) {
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
934
|
+
// Headphones: a band over the top and a cup on each ear.
|
|
935
|
+
head(-3, chin + 6, "e");
|
|
936
|
+
head(3, chin + 6, "e");
|
|
937
|
+
span(head, -2, 2, chin + 7, "e");
|
|
938
|
+
for (const x of [-4, 4]) {
|
|
939
|
+
for (let y = chin + 2; y <= chin + 4; y++) {
|
|
940
|
+
head(x, y, y === chin + 3 ? "S" : "e");
|
|
941
|
+
}
|
|
942
|
+
}
|
|
397
943
|
}
|
|
398
|
-
if (gear.
|
|
399
|
-
|
|
400
|
-
|
|
944
|
+
if (gear.headwear === 1) {
|
|
945
|
+
// A cap with its brim over the forehead.
|
|
946
|
+
span(head, -2, 2, chin + 7, "S");
|
|
947
|
+
span(head, -3, 3, chin + 6, "S");
|
|
948
|
+
head(-2, chin + 6, "T");
|
|
949
|
+
span(head, -4 + turn, 4 + turn, chin + 5, "z");
|
|
950
|
+
}
|
|
951
|
+
else if (gear.headwear === 2) {
|
|
952
|
+
// Sunglasses with a glint.
|
|
953
|
+
span(head, -3, -1, chin + 3, "e");
|
|
954
|
+
span(head, 1, 3, chin + 3, "e");
|
|
955
|
+
head(0, chin + 3, "e");
|
|
956
|
+
head(-2, chin + 3, "w");
|
|
957
|
+
}
|
|
958
|
+
}
|
|
959
|
+
// Where each foot is on every frame of a stride, toward +x.
|
|
960
|
+
const gaits = {
|
|
961
|
+
walk: {
|
|
962
|
+
feet: [4, 3, 1, -1, -3, -4, -2, 2],
|
|
963
|
+
lift: [0, 0, 0, 0, 0, 1, 2, 1],
|
|
964
|
+
bob: [1, 0, 0, 0, 1, 0, 0, 0],
|
|
965
|
+
},
|
|
966
|
+
run: {
|
|
967
|
+
feet: [5, 2, -2, -5, -2, 3],
|
|
968
|
+
lift: [0, 0, 1, 3, 4, 2],
|
|
969
|
+
bob: [1, 0, 0, 1, 0, 0],
|
|
970
|
+
},
|
|
971
|
+
};
|
|
972
|
+
function paintSideLeg({ put, b, look }, gait, index, isFar) {
|
|
973
|
+
const bottom = bottomOf[look.style];
|
|
974
|
+
const footX = gait.feet[index] ?? 0;
|
|
975
|
+
const lift = gait.lift[index] ?? 0;
|
|
976
|
+
const sole = ground + lift;
|
|
977
|
+
const hip = [0, b.hip - 1];
|
|
978
|
+
const knee = [
|
|
979
|
+
Math.round(footX / 2) + 1 + Math.min(lift, 2),
|
|
980
|
+
Math.round((b.hip + sole) / 2) + Math.floor(lift / 2),
|
|
981
|
+
];
|
|
982
|
+
const ankle = [footX, sole + bottom.shoeRows];
|
|
983
|
+
const pants = isFar ? bottom.shade : bottom.main;
|
|
984
|
+
const skin = isFar ? "q" : "s";
|
|
985
|
+
const shorts = ground + Math.round(b.legs * 0.55);
|
|
986
|
+
for (const [from, to, width] of [
|
|
987
|
+
[hip, knee, b.thigh - 1],
|
|
988
|
+
[knee, ankle, b.calf - 1],
|
|
989
|
+
]) {
|
|
990
|
+
for (const [x, y] of pointsOf(from, to)) {
|
|
991
|
+
const key = look.style === "athlete" && y < shorts ? skin : pants;
|
|
992
|
+
span(put, x - Math.floor(width / 2), x + Math.ceil(width / 2), y, key);
|
|
993
|
+
}
|
|
994
|
+
}
|
|
995
|
+
// The shoe points the way it walks, toe first.
|
|
996
|
+
for (let row = 0; row < bottom.shoeRows; row++) {
|
|
997
|
+
const key = row === 0 ? bottom.sole : bottom.shoe;
|
|
998
|
+
span(put, footX - 1, footX + (row === 0 ? 3 : 2), sole + row, key);
|
|
999
|
+
}
|
|
1000
|
+
}
|
|
1001
|
+
function paintSideHead({ upper, b, look, gear }, pose) {
|
|
1002
|
+
const { chin } = b;
|
|
1003
|
+
const [hair, light] = hairOf[look.hair];
|
|
1004
|
+
// The neck and the head in profile, nose forward and hair behind.
|
|
1005
|
+
span(upper, -1, 1, b.shoulder + 1, "s");
|
|
1006
|
+
upper(-1, b.shoulder + 1, "q");
|
|
1007
|
+
span(upper, -1, 2, chin, "s");
|
|
1008
|
+
upper(-1, chin, "q");
|
|
1009
|
+
span(upper, -2, 3, chin + 1, "s");
|
|
1010
|
+
for (let y = chin + 2; y <= chin + 4; y++) {
|
|
1011
|
+
span(upper, -3, 3, y, "s");
|
|
1012
|
+
}
|
|
1013
|
+
upper(4, chin + 2, "s");
|
|
1014
|
+
span(upper, -3, 3, chin + 5, hair);
|
|
1015
|
+
span(upper, -2, 2, chin + 6, hair);
|
|
1016
|
+
upper(0, chin + 5, light);
|
|
1017
|
+
span(upper, -3, -1, chin + 4, hair);
|
|
1018
|
+
span(upper, -3, -2, chin + 3, hair);
|
|
1019
|
+
upper(-3, chin + 2, hair);
|
|
1020
|
+
upper(-1, chin + 3, "q");
|
|
1021
|
+
upper(2, chin + 3, "e");
|
|
1022
|
+
upper(2, chin + 4, hair);
|
|
1023
|
+
upper(3, chin + 1, pose.mouth === "smile" ? "m" : "q");
|
|
1024
|
+
if (look.gender === "female" && look.style !== "athlete") {
|
|
1025
|
+
for (let y = b.shoulder - 3; y <= chin + 3; y++) {
|
|
1026
|
+
span(upper, -4, -3, y, hair);
|
|
1027
|
+
}
|
|
1028
|
+
}
|
|
1029
|
+
else if (look.gender === "female") {
|
|
1030
|
+
const sway = Math.floor(pose.frame / 2) % 2;
|
|
1031
|
+
upper(-4, chin + 4, hair);
|
|
1032
|
+
upper(-5, chin + 3 - sway, hair);
|
|
1033
|
+
upper(-5, chin + 2 - sway, light);
|
|
1034
|
+
}
|
|
1035
|
+
else if (look.gender === "other") {
|
|
1036
|
+
for (let y = chin + 1; y <= chin + 3; y++) {
|
|
1037
|
+
upper(-3, y, hair);
|
|
1038
|
+
upper(-4, y, hair);
|
|
1039
|
+
}
|
|
1040
|
+
}
|
|
1041
|
+
if (gear.gadget >= 1) {
|
|
1042
|
+
span(upper, -1, 1, chin + 7, "e");
|
|
1043
|
+
for (let y = chin + 2; y <= chin + 4; y++) {
|
|
1044
|
+
upper(-1, y, y === chin + 3 ? "S" : "e");
|
|
1045
|
+
}
|
|
401
1046
|
}
|
|
402
1047
|
if (gear.headwear === 1) {
|
|
403
|
-
span(
|
|
404
|
-
span(
|
|
405
|
-
span(
|
|
1048
|
+
span(upper, -3, 2, chin + 6, "S");
|
|
1049
|
+
span(upper, -2, 1, chin + 7, "S");
|
|
1050
|
+
span(upper, 2, 5, chin + 5, "z");
|
|
406
1051
|
}
|
|
407
1052
|
else if (gear.headwear === 2) {
|
|
408
|
-
span(
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
1053
|
+
span(upper, 1, 3, chin + 3, "e");
|
|
1054
|
+
upper(0, chin + 4, "e");
|
|
1055
|
+
}
|
|
1056
|
+
}
|
|
1057
|
+
// The body in profile, facing +x, on frame `frame` of the gait.
|
|
1058
|
+
function paintSide(paint, pose) {
|
|
1059
|
+
const { put, b, look, gear } = paint;
|
|
1060
|
+
const gait = gaits[pose.gait];
|
|
1061
|
+
const count = gait.feet.length;
|
|
1062
|
+
const frame = pose.frame % count;
|
|
1063
|
+
const other = (frame + count / 2) % count;
|
|
1064
|
+
const bob = gait.bob[frame] ?? 0;
|
|
1065
|
+
const upper = (x, y, key) => {
|
|
1066
|
+
put(x, y - bob, key);
|
|
1067
|
+
};
|
|
1068
|
+
const sideways = { ...paint, upper };
|
|
1069
|
+
const top = topOf[look.style];
|
|
1070
|
+
const back = Math.max(b.chest - 2, 2);
|
|
1071
|
+
const front = b.chest - 1;
|
|
1072
|
+
// Arms swing against the legs; running bends them at the elbow.
|
|
1073
|
+
const arm = (index, isFar) => {
|
|
1074
|
+
const swing = -(gait.feet[index] ?? 0);
|
|
1075
|
+
const hand = pose.gait === "run"
|
|
1076
|
+
? [Math.round(swing * 0.7) + 2, b.shoulder - Math.round(b.reach / 2)]
|
|
1077
|
+
: [Math.round(swing * 0.75), b.shoulder - b.reach + 1];
|
|
1078
|
+
paintArm({ ...sideways, b: { ...b, chest: -1 } }, 1, hand, isFar ? "far" : "near");
|
|
1079
|
+
};
|
|
1080
|
+
// The far arm and leg first, in shadow.
|
|
1081
|
+
arm(other, true);
|
|
1082
|
+
paintSideLeg(paint, gait, other, true);
|
|
1083
|
+
paintSideLeg(paint, gait, frame, false);
|
|
1084
|
+
// The torso, deeper at the chest, with a belly or hips where they are.
|
|
1085
|
+
const torso = b.shoulder - b.hip + 1;
|
|
1086
|
+
const chestRows = Math.ceil(torso * 0.45);
|
|
1087
|
+
for (let y = b.hip; y <= b.shoulder; y++) {
|
|
1088
|
+
const isChest = b.shoulder - y < chestRows;
|
|
1089
|
+
let forward = front;
|
|
1090
|
+
let behind = back;
|
|
1091
|
+
if (!isChest && look.build === "heavy") {
|
|
1092
|
+
forward += 2;
|
|
1093
|
+
}
|
|
1094
|
+
else if (!isChest && look.build === "stocky") {
|
|
1095
|
+
forward += 1;
|
|
1096
|
+
}
|
|
1097
|
+
if (y - b.hip < 2 && look.gender === "female") {
|
|
1098
|
+
behind += 1;
|
|
1099
|
+
}
|
|
1100
|
+
span(upper, -behind, forward, y, top.main);
|
|
1101
|
+
upper(-behind, y, top.shade);
|
|
1102
|
+
upper(forward, y, top.light);
|
|
1103
|
+
}
|
|
1104
|
+
if (look.style === "standing") {
|
|
1105
|
+
for (let y = b.hip - 3; y <= b.shoulder - 2; y++) {
|
|
1106
|
+
upper(front, y, "S");
|
|
1107
|
+
upper(front - 1, y, "S");
|
|
1108
|
+
}
|
|
1109
|
+
}
|
|
1110
|
+
else if (look.style === "desk") {
|
|
1111
|
+
span(upper, -2, -1, b.shoulder + 1, top.shade);
|
|
1112
|
+
span(upper, 0, front, b.hip + 2, top.shade);
|
|
413
1113
|
}
|
|
414
|
-
if (gear.
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
1114
|
+
if (gear.wear === 2) {
|
|
1115
|
+
for (let y = b.hip; y <= b.shoulder; y++) {
|
|
1116
|
+
span(upper, -back, front - 2, y, "X");
|
|
1117
|
+
}
|
|
1118
|
+
}
|
|
1119
|
+
if (gear.carry >= 1) {
|
|
1120
|
+
// The backpack rides behind.
|
|
1121
|
+
for (let y = b.shoulder - 6; y <= b.shoulder - 1; y++) {
|
|
1122
|
+
span(upper, -back - 3, -back - 1, y, y === b.shoulder - 1 ? "b" : "B");
|
|
1123
|
+
}
|
|
1124
|
+
upper(-back - 2, b.shoulder - 3, "d");
|
|
418
1125
|
}
|
|
1126
|
+
if (gear.carry === 2) {
|
|
1127
|
+
upper(front, b.shoulder - 3, "g");
|
|
1128
|
+
upper(front - 1, b.shoulder - 1, "z");
|
|
1129
|
+
}
|
|
1130
|
+
paintSideHead(sideways, pose);
|
|
1131
|
+
if (gear.wear >= 1) {
|
|
1132
|
+
span(upper, -2, 2, b.shoulder + 1, "S");
|
|
1133
|
+
const flutter = Math.floor(pose.frame / 2) % 2;
|
|
1134
|
+
upper(-3, b.shoulder + 1 - flutter, "z");
|
|
1135
|
+
upper(-4, b.shoulder - flutter, "z");
|
|
1136
|
+
}
|
|
1137
|
+
// The near arm swings in front of the body.
|
|
1138
|
+
arm(frame, false);
|
|
419
1139
|
}
|
|
1140
|
+
function paintFront(paint, pose) {
|
|
1141
|
+
const { b } = paint;
|
|
1142
|
+
paintHairBehind(paint, pose.nod ?? 0);
|
|
1143
|
+
paintLegsFront(paint, pose);
|
|
1144
|
+
paintTorsoFront(paint);
|
|
1145
|
+
for (const [index, side] of [-1, 1].entries()) {
|
|
1146
|
+
paintArm(paint, side, pose.hands?.[index] ?? hanging(b, side));
|
|
1147
|
+
}
|
|
1148
|
+
paintHeadFront(paint, pose);
|
|
1149
|
+
}
|
|
1150
|
+
const mix = (from, to, amount) => {
|
|
1151
|
+
const parse = (hex) => [1, 3, 5].map((index) => Number.parseInt(hex.slice(index, index + 2), 16));
|
|
1152
|
+
const [start, end] = [parse(from), parse(to)];
|
|
1153
|
+
return `#${start
|
|
1154
|
+
.map((value, index) => Math.round(value + ((end[index] ?? value) - value) * amount)
|
|
1155
|
+
.toString(16)
|
|
1156
|
+
.padStart(2, "0"))
|
|
1157
|
+
.join("")}`;
|
|
1158
|
+
};
|
|
420
1159
|
export function paletteOf(palette) {
|
|
421
|
-
const { cloak, cloakShade, glow } = palette.sprite;
|
|
1160
|
+
const { cloak, cloakShade, scarf, scarfShade, glow } = palette.sprite;
|
|
422
1161
|
return new Map([
|
|
423
|
-
["
|
|
1162
|
+
["A", "#141414"],
|
|
1163
|
+
["B", "#3E3226"],
|
|
1164
|
+
["b", "#6B4A2E"],
|
|
1165
|
+
["C", cloak],
|
|
1166
|
+
["c", cloakShade],
|
|
424
1167
|
["D", "#50555F"],
|
|
425
|
-
["d", "#
|
|
426
|
-
["
|
|
1168
|
+
["d", "#9AA0AA"],
|
|
1169
|
+
["E", mix(cloak, "#FFFFFF", 0.16)],
|
|
1170
|
+
["e", "#2B2B2B"],
|
|
427
1171
|
["F", "#C8473F"],
|
|
428
1172
|
["G", "#6FA35A"],
|
|
429
1173
|
["g", "#D9B45A"],
|
|
430
|
-
["
|
|
431
|
-
["
|
|
1174
|
+
["H", "#9C9C9C"],
|
|
1175
|
+
["h", "#4A3222"],
|
|
1176
|
+
["I", "#E4E4E4"],
|
|
1177
|
+
["i", "#6B4A33"],
|
|
1178
|
+
["J", "#BDBDBD"],
|
|
1179
|
+
["K", "#C4C4C4"],
|
|
432
1180
|
["L", "#4A505B"],
|
|
433
1181
|
["l", glow],
|
|
434
|
-
["
|
|
435
|
-
["
|
|
1182
|
+
["M", "#424A58"],
|
|
1183
|
+
["m", "#B5524A"],
|
|
1184
|
+
["N", "#B59A6A"],
|
|
1185
|
+
["n", "#937B52"],
|
|
1186
|
+
["O", "#5E6B45"],
|
|
1187
|
+
["o", "#5A3A25"],
|
|
1188
|
+
["P", "#3E6870"],
|
|
1189
|
+
["p", "#33565D"],
|
|
436
1190
|
["Q", "#E6E0D4"],
|
|
437
|
-
["
|
|
438
|
-
["
|
|
439
|
-
["
|
|
440
|
-
["
|
|
1191
|
+
["q", "#C99872"],
|
|
1192
|
+
["R", "#4F8189"],
|
|
1193
|
+
["r", "#E8909A"],
|
|
1194
|
+
["S", scarf],
|
|
1195
|
+
["s", "#E2B48C"],
|
|
1196
|
+
["T", mix(scarf, "#FFFFFF", 0.22)],
|
|
441
1197
|
["t", glow],
|
|
1198
|
+
["U", "#B4AE9F"],
|
|
1199
|
+
["V", "#4A5536"],
|
|
442
1200
|
["v", "#9AA0A8"],
|
|
443
1201
|
["w", "#F2F2F2"],
|
|
1202
|
+
["X", "#2F3540"],
|
|
444
1203
|
["x", "#3D414B"],
|
|
1204
|
+
["Y", "#C8C8C8"],
|
|
445
1205
|
["y", palette.soft],
|
|
446
|
-
["
|
|
1206
|
+
["Z", "#C8C8C8"],
|
|
1207
|
+
["z", scarfShade],
|
|
447
1208
|
]);
|
|
448
1209
|
}
|
|
449
1210
|
// The pixel keys of 1 frame, row by row, "." where nothing is drawn.
|
|
450
|
-
export function composeScene({ activity, counter, level, look, mood,
|
|
1211
|
+
export function composeScene({ activity, counter, level, look, mood, width, anchor = homeOf(width), }) {
|
|
451
1212
|
const canvas = Array.from({ length: stageHeight }, () => Array.from({ length: width }, () => "."));
|
|
452
|
-
const
|
|
453
|
-
|
|
1213
|
+
const scene = sceneOf(activity, counter, width, mood, anchor);
|
|
1214
|
+
const b = bodyOf(look);
|
|
1215
|
+
const pose = scene.pose(b);
|
|
1216
|
+
// The character's x = 0 is its center column, mirrored when facing left.
|
|
1217
|
+
const plant = (rise) => (x, y, key) => {
|
|
1218
|
+
const column = scene.x + scene.facing * x;
|
|
1219
|
+
const row = canvas[stageHeight - 1 - (y + rise)];
|
|
454
1220
|
if (row && column >= 0 && column < width && key !== ".") {
|
|
455
1221
|
row[column] = key;
|
|
456
1222
|
}
|
|
457
1223
|
};
|
|
458
|
-
const
|
|
459
|
-
const
|
|
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
|
-
};
|
|
1224
|
+
const put = plant(scene.lift);
|
|
1225
|
+
const bob = pose.view === "front" ? (pose.bob ?? 0) : 0;
|
|
467
1226
|
const paint = {
|
|
468
1227
|
put,
|
|
469
|
-
|
|
1228
|
+
upper: plant(scene.lift - bob),
|
|
1229
|
+
b,
|
|
470
1230
|
look,
|
|
471
1231
|
gear: gearOf(level),
|
|
472
|
-
pose: scene.pose,
|
|
473
1232
|
counter,
|
|
474
1233
|
};
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
1234
|
+
paintShadow(plant(0), b, scene.lift);
|
|
1235
|
+
if (pose.view === "side") {
|
|
1236
|
+
paintSide(paint, pose);
|
|
1237
|
+
}
|
|
1238
|
+
else {
|
|
1239
|
+
paintFront(paint, pose);
|
|
1240
|
+
}
|
|
481
1241
|
for (const draw of scene.props ?? []) {
|
|
482
|
-
draw(put,
|
|
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);
|
|
1242
|
+
draw(put, paint.upper, b);
|
|
492
1243
|
}
|
|
493
1244
|
return canvas.map((row) => row.join(""));
|
|
494
1245
|
}
|