@siegesailor/terminal-sigma 1.5.0 → 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 +30 -25
- package/build/app.js +13 -30
- package/build/app.js.map +1 -1
- package/build/components/character.js +25 -8
- 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 +53 -20
- package/build/content.js.map +1 -1
- package/build/health.js +5 -2
- package/build/health.js.map +1 -1
- package/build/i18n.js +36 -36
- package/build/i18n.js.map +1 -1
- package/build/pixel-art.js +897 -442
- package/build/pixel-art.js.map +1 -1
- package/build/progress.js +1 -0
- package/build/progress.js.map +1 -1
- package/package.json +1 -1
package/build/pixel-art.js
CHANGED
|
@@ -1,30 +1,50 @@
|
|
|
1
|
-
// The character stands on a stage as wide as its panel and
|
|
1
|
+
// The character stands on a stage as wide as its panel and 40 pixels tall.
|
|
2
2
|
// Each terminal cell draws 2 stacked pixels with "▀", foreground for the top
|
|
3
|
-
// and background for the bottom, so the stage takes
|
|
4
|
-
export const stageHeight =
|
|
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;
|
|
5
7
|
export const defaultLook = {
|
|
6
|
-
height:
|
|
8
|
+
height: 29,
|
|
7
9
|
build: "average",
|
|
8
10
|
gender: "other",
|
|
9
11
|
style: "desk",
|
|
10
12
|
hair: "dark",
|
|
13
|
+
isMuscular: false,
|
|
11
14
|
};
|
|
12
|
-
//
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
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) {
|
|
17
23
|
const bmi = profile.weight / (profile.height / 100) ** 2;
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
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";
|
|
21
32
|
}
|
|
22
|
-
|
|
23
|
-
|
|
33
|
+
const [lean, soft, heavy] = fatBands[profile.gender];
|
|
34
|
+
if (profile.bodyFat < lean) {
|
|
35
|
+
return bmi >= 23 ? "athletic" : "slim";
|
|
24
36
|
}
|
|
25
|
-
|
|
26
|
-
|
|
37
|
+
if (profile.bodyFat >= heavy) {
|
|
38
|
+
return "heavy";
|
|
27
39
|
}
|
|
40
|
+
return profile.bodyFat >= soft ? "stocky" : "average";
|
|
41
|
+
}
|
|
42
|
+
// 145 cm draws 26 pixels tall and 205 cm 34.
|
|
43
|
+
export function lookOf(profile) {
|
|
44
|
+
if (!profile) {
|
|
45
|
+
return defaultLook;
|
|
46
|
+
}
|
|
47
|
+
const build = buildOf(profile);
|
|
28
48
|
let hair = "dark";
|
|
29
49
|
if (profile.age >= 70) {
|
|
30
50
|
hair = "white";
|
|
@@ -33,12 +53,12 @@ export function lookOf(profile) {
|
|
|
33
53
|
hair = "grey";
|
|
34
54
|
}
|
|
35
55
|
return {
|
|
36
|
-
height: Math.min(Math.max(Math.round(
|
|
56
|
+
height: Math.min(Math.max(Math.round(26 + (profile.height - 145) / 7.5), 24), 35),
|
|
37
57
|
build,
|
|
38
58
|
gender: profile.gender,
|
|
39
59
|
style: profile.workStyle,
|
|
40
60
|
hair,
|
|
41
|
-
|
|
61
|
+
isMuscular: build === "athletic" || profile.goal === "buildMuscle",
|
|
42
62
|
};
|
|
43
63
|
}
|
|
44
64
|
// Each equipment slot upgrades once from level 2 and again 4 levels later,
|
|
@@ -47,43 +67,55 @@ export const gearOf = (level) => {
|
|
|
47
67
|
const tier = (slot) => Math.min(Math.max(Math.floor((level - 2 - slot) / 4) + 1, 0), 2);
|
|
48
68
|
return { gadget: tier(0), wear: tier(1), headwear: tier(2), carry: tier(3) };
|
|
49
69
|
};
|
|
50
|
-
// The feet stand on row
|
|
51
|
-
const ground =
|
|
52
|
-
const
|
|
53
|
-
female: [
|
|
54
|
-
male: [
|
|
55
|
-
other: [
|
|
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],
|
|
56
76
|
};
|
|
57
|
-
// A head
|
|
77
|
+
// A 7-row head and a neck take 8 of the height; legs take 55% of the rest.
|
|
58
78
|
function bodyOf(look) {
|
|
59
|
-
const span = look.height -
|
|
60
|
-
const legs = Math.round(span * 0.
|
|
79
|
+
const span = look.height - 8;
|
|
80
|
+
const legs = Math.round(span * 0.55);
|
|
61
81
|
const torso = span - legs;
|
|
62
|
-
let [chest, waist, hips] =
|
|
82
|
+
let [chest, waist, hips] = frames[look.gender];
|
|
63
83
|
switch (look.build) {
|
|
64
84
|
case "slim": {
|
|
65
85
|
chest -= look.gender === "male" ? 1 : 0;
|
|
66
86
|
waist = Math.max(waist - 1, 2);
|
|
67
|
-
hips = Math.max(hips - 1,
|
|
87
|
+
hips = Math.max(hips - 1, 3);
|
|
88
|
+
break;
|
|
89
|
+
}
|
|
90
|
+
case "athletic": {
|
|
91
|
+
chest += 1;
|
|
68
92
|
break;
|
|
69
93
|
}
|
|
70
94
|
case "stocky": {
|
|
71
95
|
waist += 1;
|
|
96
|
+
hips += 1;
|
|
72
97
|
break;
|
|
73
98
|
}
|
|
74
99
|
case "heavy": {
|
|
75
100
|
chest += 1;
|
|
76
|
-
waist +=
|
|
77
|
-
hips +=
|
|
101
|
+
waist += 3;
|
|
102
|
+
hips += 2;
|
|
78
103
|
break;
|
|
79
104
|
}
|
|
80
105
|
case "average": {
|
|
81
106
|
break;
|
|
82
107
|
}
|
|
83
108
|
}
|
|
84
|
-
if (look.
|
|
109
|
+
if (look.isMuscular && look.build !== "athletic") {
|
|
85
110
|
chest += 1;
|
|
86
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
|
+
}
|
|
87
119
|
const hip = ground + legs;
|
|
88
120
|
const shoulder = hip + torso - 1;
|
|
89
121
|
const chin = shoulder + 2;
|
|
@@ -91,17 +123,22 @@ function bodyOf(look) {
|
|
|
91
123
|
hip,
|
|
92
124
|
shoulder,
|
|
93
125
|
chin,
|
|
94
|
-
top: chin +
|
|
126
|
+
top: chin + 6,
|
|
95
127
|
legs,
|
|
128
|
+
knee: ground + Math.floor(legs / 2),
|
|
129
|
+
shin: ground + Math.floor(legs / 4),
|
|
96
130
|
chest,
|
|
97
131
|
waist,
|
|
98
132
|
hips,
|
|
99
|
-
|
|
100
|
-
|
|
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,
|
|
101
138
|
};
|
|
102
139
|
}
|
|
103
140
|
const span = (put, from, to, y, key) => {
|
|
104
|
-
for (let x = from; x <= to; x++) {
|
|
141
|
+
for (let x = Math.min(from, to); x <= Math.max(from, to); x++) {
|
|
105
142
|
put(x, y, key);
|
|
106
143
|
}
|
|
107
144
|
};
|
|
@@ -116,431 +153,561 @@ const pointsOf = ([x0, y0], [x1, y1]) => {
|
|
|
116
153
|
}
|
|
117
154
|
return points;
|
|
118
155
|
};
|
|
156
|
+
// Where a hand rests with the arm hanging, nudged by `swing`.
|
|
119
157
|
const hanging = (b, side, swing = 0) => [
|
|
120
158
|
side * (b.chest + 1),
|
|
121
|
-
b.shoulder - b.reach + swing,
|
|
159
|
+
b.shoulder - b.reach + 1 + swing,
|
|
122
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 };
|
|
208
|
+
}
|
|
209
|
+
remaining -= Math.abs(to - from);
|
|
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];
|
|
216
|
+
};
|
|
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);
|
|
233
|
+
}
|
|
234
|
+
return cycles[activity];
|
|
235
|
+
}
|
|
236
|
+
const smileOf = (mood) => (mood === "happy" ? "smile" : undefined);
|
|
123
237
|
// A standing desk at the waist, with the laptop's lid facing out.
|
|
124
238
|
const desk = (put, _upper, b) => {
|
|
125
239
|
const top = b.hip + 1;
|
|
126
|
-
span(put, -
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
put(
|
|
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");
|
|
130
245
|
}
|
|
131
|
-
for (let y = top + 1; y <= top +
|
|
132
|
-
span(put, -
|
|
246
|
+
for (let y = top + 1; y <= top + 4; y++) {
|
|
247
|
+
span(put, -3, 3, y, "d");
|
|
133
248
|
}
|
|
134
|
-
put
|
|
249
|
+
span(put, -3, 3, top + 1, "D");
|
|
250
|
+
put(0, top + 3, "l");
|
|
135
251
|
};
|
|
136
|
-
const
|
|
137
|
-
span(upper, x, x +
|
|
138
|
-
span(upper, x, x +
|
|
139
|
-
upper
|
|
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");
|
|
140
258
|
if (steam !== undefined) {
|
|
141
|
-
|
|
142
|
-
upper(x +
|
|
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");
|
|
143
263
|
}
|
|
144
264
|
};
|
|
145
265
|
const apple = ([x, y], isBitten) => (_put, upper) => {
|
|
146
|
-
span(upper, x, x +
|
|
147
|
-
upper
|
|
148
|
-
upper
|
|
149
|
-
upper(x +
|
|
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");
|
|
150
273
|
};
|
|
151
274
|
const barbell = (y) => (_put, upper, b) => {
|
|
152
|
-
const end = b.chest +
|
|
275
|
+
const end = b.chest + 7;
|
|
153
276
|
span(upper, -end, end, y, "d");
|
|
154
277
|
for (const x of [-end, -end + 1, end - 1, end]) {
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
278
|
+
for (let dy = -2; dy <= 2; dy++) {
|
|
279
|
+
upper(x, y + dy, Math.abs(x) === end ? "D" : "d");
|
|
280
|
+
}
|
|
158
281
|
}
|
|
159
282
|
};
|
|
160
|
-
const sparkles = (
|
|
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
|
+
];
|
|
161
299
|
for (const [x, y] of points) {
|
|
162
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");
|
|
163
305
|
}
|
|
164
306
|
};
|
|
165
|
-
const sleepyZ = (
|
|
307
|
+
const sleepyZ = (t) => (put, _upper, b) => {
|
|
308
|
+
const rise = Math.floor((t % 24) / 3);
|
|
166
309
|
const rows = ["ZZZZ", "..Z.", ".Z..", "ZZZZ"];
|
|
167
310
|
for (const [offset, row] of rows.entries()) {
|
|
168
311
|
for (const [index, key] of [...row].entries()) {
|
|
169
|
-
put(
|
|
312
|
+
put(4 + (rise % 2) + index, b.top + 1 + rise - offset, key);
|
|
170
313
|
}
|
|
171
314
|
}
|
|
172
315
|
};
|
|
173
|
-
// Standing
|
|
174
|
-
// waving when every target is met.
|
|
316
|
+
// Standing still between walks: breathing, blinking, looking about, shaking
|
|
317
|
+
// out the hands, and waving when every target is met.
|
|
175
318
|
const resting = (t, mood) => (b) => {
|
|
176
|
-
const isWaving = mood === "happy" && t >= 3 && t <= 5;
|
|
177
319
|
let turn = 0;
|
|
178
|
-
if (t
|
|
320
|
+
if (t >= 8 && t < 12) {
|
|
179
321
|
turn = -1;
|
|
180
322
|
}
|
|
181
|
-
else if (t
|
|
323
|
+
else if (t >= 12 && t < 16) {
|
|
182
324
|
turn = 1;
|
|
183
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
|
+
}
|
|
184
339
|
return {
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
340
|
+
view: "front",
|
|
341
|
+
mouth: smileOf(mood),
|
|
342
|
+
bob: breathing(t),
|
|
343
|
+
eyes: t === 5 || t === 6 ? "closed" : "open",
|
|
188
344
|
turn,
|
|
189
|
-
|
|
190
|
-
hands: isWaving
|
|
191
|
-
? [undefined, [b.chest + 3 + (t % 2), b.top - 1]]
|
|
192
|
-
: undefined,
|
|
345
|
+
hands,
|
|
193
346
|
};
|
|
194
347
|
};
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
const minimum = 8;
|
|
209
|
-
const maximum = Math.max(width - 9, minimum + 1);
|
|
210
|
-
const travel = maximum - minimum;
|
|
211
|
-
const tick = counter % 4;
|
|
212
|
-
const mouth = mood === "happy" ? "smile" : undefined;
|
|
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
|
+
});
|
|
213
361
|
if (activity === "idle" && mood === "sleepy") {
|
|
214
|
-
// Dozing on its feet: nodding, yawning, with Zs drifting up.
|
|
215
|
-
const
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
hands: isYawning ? [undefined, [2, b.chin]] : undefined,
|
|
225
|
-
}),
|
|
226
|
-
props: [sleepyZ(at)],
|
|
227
|
-
};
|
|
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)]);
|
|
228
372
|
}
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
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
|
+
}
|
|
241
389
|
return {
|
|
242
|
-
x:
|
|
243
|
-
facing:
|
|
244
|
-
|
|
390
|
+
x: place.x,
|
|
391
|
+
facing: 1,
|
|
392
|
+
lift: mood === "happy" && place.resting === 1 ? 1 : 0,
|
|
393
|
+
pose: resting(place.resting, mood),
|
|
245
394
|
};
|
|
246
395
|
}
|
|
247
|
-
const step = at - 2 * pause - travel;
|
|
248
|
-
return { x: maximum - step, facing: -1, pose: walking(step, mood) };
|
|
249
|
-
}
|
|
250
|
-
switch (activity) {
|
|
251
396
|
case "run": {
|
|
252
|
-
//
|
|
253
|
-
const
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
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,
|
|
401
|
+
};
|
|
257
402
|
return {
|
|
258
|
-
x:
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
pose: (b) => ({
|
|
263
|
-
mouth,
|
|
264
|
-
turn: 1,
|
|
265
|
-
step: counter % 2 === 0 ? 1 : 3,
|
|
266
|
-
bob: counter % 2,
|
|
267
|
-
hands: [
|
|
268
|
-
[-(b.chest + 2), b.hip + 2 + (counter % 2)],
|
|
269
|
-
[b.chest + 2, b.hip + 3 - (counter % 2)],
|
|
270
|
-
],
|
|
271
|
-
}),
|
|
403
|
+
x: place.x,
|
|
404
|
+
facing: place.facing,
|
|
405
|
+
lift: 0,
|
|
406
|
+
pose: () => ({ view: "side", frame: at, gait: "run", mouth }),
|
|
272
407
|
props: [
|
|
273
408
|
(put, _upper, b) => {
|
|
274
|
-
put(-(b.chest +
|
|
275
|
-
put(-(b.chest +
|
|
409
|
+
put(-(b.chest + 3), b.top - 1 - (at % 2), "v");
|
|
410
|
+
put(-(b.chest + 5), b.hip + (at % 3), "v");
|
|
276
411
|
},
|
|
277
412
|
],
|
|
278
413
|
};
|
|
279
414
|
}
|
|
280
415
|
case "focus": {
|
|
281
|
-
// Typing at a standing desk, the
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
[3, b.hip + 3 - (tick % 2)],
|
|
291
|
-
],
|
|
292
|
-
}),
|
|
293
|
-
props: [
|
|
294
|
-
desk,
|
|
295
|
-
(put, _upper, b) => {
|
|
296
|
-
put(-3 + tick, b.top + 2 + (tick % 2), "y");
|
|
297
|
-
},
|
|
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],
|
|
298
425
|
],
|
|
299
|
-
}
|
|
426
|
+
}), [
|
|
427
|
+
desk,
|
|
428
|
+
(put, _upper, b) => {
|
|
429
|
+
put(-2 + (at % 5), b.top + 2 + key, "y");
|
|
430
|
+
},
|
|
431
|
+
]);
|
|
300
432
|
}
|
|
301
433
|
case "break": {
|
|
302
|
-
// A coffee, sipped every few
|
|
303
|
-
const
|
|
304
|
-
const isSipping = at >= 4;
|
|
434
|
+
// A coffee, sipped every few seconds.
|
|
435
|
+
const isSipping = at >= 28;
|
|
305
436
|
const hand = (b) => isSipping ? [2, b.chin] : [b.chest + 2, b.hip + 2];
|
|
306
|
-
return {
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
},
|
|
319
|
-
],
|
|
320
|
-
};
|
|
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);
|
|
447
|
+
},
|
|
448
|
+
]);
|
|
321
449
|
}
|
|
322
450
|
case "eat": {
|
|
323
|
-
|
|
324
|
-
const
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
],
|
|
339
|
-
};
|
|
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);
|
|
464
|
+
},
|
|
465
|
+
]);
|
|
340
466
|
}
|
|
341
467
|
case "lift": {
|
|
342
|
-
// An overhead press: rack, press, lock out, lower.
|
|
343
|
-
const height = (b) =>
|
|
344
|
-
b.shoulder;
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
[x, y],
|
|
357
|
-
],
|
|
358
|
-
};
|
|
359
|
-
},
|
|
360
|
-
props: [
|
|
361
|
-
(put, upper, b) => {
|
|
362
|
-
barbell(height(b))(put, upper, b);
|
|
363
|
-
},
|
|
364
|
-
],
|
|
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);
|
|
365
482
|
};
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
[
|
|
376
|
-
[
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
bob: tick === 1 ? 0 : 1,
|
|
383
|
-
lift: tick === 1 ? 1 : 0,
|
|
384
|
-
eyes: tick === 1 ? "closed" : "open",
|
|
385
|
-
hands: [
|
|
386
|
-
[-x, y],
|
|
387
|
-
[x, y],
|
|
388
|
-
],
|
|
389
|
-
};
|
|
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);
|
|
390
499
|
},
|
|
391
|
-
|
|
500
|
+
]);
|
|
392
501
|
}
|
|
393
|
-
case "
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
502
|
+
case "stretch": {
|
|
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",
|
|
401
518
|
hands: [
|
|
402
|
-
[-
|
|
403
|
-
[
|
|
519
|
+
[-x, phase === 3 ? y + 1 - (at % 2) : y],
|
|
520
|
+
[x, y],
|
|
404
521
|
],
|
|
405
|
-
}
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
[-8, b.shoulder - 2],
|
|
419
|
-
[8, b.shoulder],
|
|
420
|
-
])(put, upper, b);
|
|
421
|
-
},
|
|
522
|
+
};
|
|
523
|
+
}, undefined, phase === 1 ? 1 : 0);
|
|
524
|
+
}
|
|
525
|
+
case "levelUp": {
|
|
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],
|
|
422
535
|
],
|
|
423
|
-
};
|
|
536
|
+
}), [sparkles(at)], lift);
|
|
424
537
|
}
|
|
425
538
|
}
|
|
426
539
|
}
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
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
|
+
}
|
|
445
557
|
}
|
|
446
|
-
return
|
|
558
|
+
return { ...motion, tick: motion.tick + 1 };
|
|
447
559
|
}
|
|
448
560
|
// The tops by work style: a hoodie, a shirt under an apron, a tee, a tank.
|
|
449
561
|
const topOf = {
|
|
450
|
-
desk:
|
|
451
|
-
standing:
|
|
452
|
-
active:
|
|
453
|
-
athlete:
|
|
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" },
|
|
454
566
|
};
|
|
455
567
|
// How much of the arm the sleeve covers, from the shoulder down.
|
|
456
568
|
const sleeveOf = {
|
|
457
569
|
desk: 1,
|
|
458
570
|
standing: 0.5,
|
|
459
|
-
active: 0.
|
|
571
|
+
active: 0.35,
|
|
460
572
|
athlete: 0,
|
|
461
573
|
};
|
|
462
574
|
// Jeans and sneakers, chinos and shoes, cargo pants and boots, or shorts
|
|
463
575
|
// and running shoes.
|
|
464
576
|
const bottomOf = {
|
|
465
|
-
desk: {
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
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"],
|
|
469
614
|
};
|
|
470
|
-
const hairKeys = { dark: "h", grey: "H", white: "I" };
|
|
471
615
|
// An oval on the ground with the feet in its middle, shrinking in the air.
|
|
472
616
|
function paintShadow(put, b, lift) {
|
|
473
|
-
const half = b.hips +
|
|
474
|
-
span(put, -half +
|
|
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");
|
|
475
620
|
span(put, -half, half, ground, "A");
|
|
476
621
|
span(put, -half + 2, half - 2, ground + 1, "A");
|
|
477
622
|
}
|
|
478
623
|
// Long hair falls behind the shoulders, so it goes on before the body.
|
|
479
|
-
function paintHairBehind({ upper, b, look, counter }) {
|
|
480
|
-
const hair =
|
|
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);
|
|
633
|
+
}
|
|
634
|
+
return;
|
|
635
|
+
}
|
|
481
636
|
if (look.gender !== "female") {
|
|
482
637
|
return;
|
|
483
638
|
}
|
|
484
639
|
if (look.style === "athlete") {
|
|
485
640
|
// A ponytail that swings.
|
|
486
|
-
const sway = counter
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
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);
|
|
491
646
|
return;
|
|
492
647
|
}
|
|
493
|
-
for (let y = b.shoulder -
|
|
494
|
-
|
|
495
|
-
|
|
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);
|
|
496
651
|
}
|
|
497
|
-
for (let y = b.shoulder -
|
|
498
|
-
span(upper, -
|
|
652
|
+
for (let y = b.shoulder - 2; y < b.chin; y++) {
|
|
653
|
+
span(upper, -3, 3, y, hair);
|
|
499
654
|
}
|
|
500
655
|
}
|
|
501
|
-
function
|
|
502
|
-
const
|
|
503
|
-
const
|
|
504
|
-
|
|
505
|
-
|
|
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;
|
|
661
|
+
}
|
|
662
|
+
if (look.style === "athlete" && at.y < shorts) {
|
|
663
|
+
return at.y === b.knee - 1 && at.x === 2 ? "q" : "s";
|
|
664
|
+
}
|
|
665
|
+
if (at.x === at.width) {
|
|
666
|
+
return side < 0 ? bottom.light : bottom.shade;
|
|
667
|
+
}
|
|
668
|
+
return at.y === at.sole + bottom.shoeRows ? bottom.shade : bottom.main;
|
|
669
|
+
}
|
|
670
|
+
function paintLegsFront({ put, b, look }, pose) {
|
|
671
|
+
const bottom = bottomOf[look.style];
|
|
506
672
|
const shorts = ground + Math.round(b.legs * 0.55);
|
|
507
673
|
for (const side of [-1, 1]) {
|
|
508
674
|
const isLifted = (side === -1 && pose.step === 1) || (side === 1 && pose.step === 3);
|
|
509
675
|
const sole = ground + (isLifted ? 1 : 0);
|
|
510
676
|
for (let y = sole; y < b.hip; y++) {
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
key = shoe;
|
|
677
|
+
let width = b.ankle;
|
|
678
|
+
if (y < sole + bottom.shoeRows) {
|
|
679
|
+
width = b.ankle + 1;
|
|
515
680
|
}
|
|
516
|
-
else if (
|
|
517
|
-
|
|
681
|
+
else if (y >= b.knee) {
|
|
682
|
+
width = b.thigh;
|
|
518
683
|
}
|
|
519
|
-
|
|
520
|
-
|
|
684
|
+
else if (y >= b.shin) {
|
|
685
|
+
width = b.calf;
|
|
521
686
|
}
|
|
522
|
-
|
|
523
|
-
put(side *
|
|
687
|
+
for (let x = 1; x <= width; x++) {
|
|
688
|
+
put(side * x, y, legKeyOf(look, b, { x, y, sole, width }, side));
|
|
524
689
|
}
|
|
525
690
|
}
|
|
526
|
-
// Shoes stick out a little past the ankle.
|
|
527
|
-
put(side * (calf + 1), sole, shoe);
|
|
528
691
|
if (look.style === "active") {
|
|
529
|
-
put(side * thigh, knee + 1, "V");
|
|
530
|
-
put(side * thigh, knee + 2, "V");
|
|
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");
|
|
695
|
+
}
|
|
696
|
+
if (look.style === "athlete") {
|
|
697
|
+
put(side * b.thigh, shorts + 1, "S");
|
|
531
698
|
}
|
|
532
699
|
}
|
|
533
700
|
// The thighs meet at the crotch, and all the way down on a heavy build.
|
|
534
|
-
let crotch = b.hip -
|
|
701
|
+
let crotch = b.hip - 2;
|
|
535
702
|
if (look.build === "heavy") {
|
|
536
|
-
crotch = look.style === "athlete" ? shorts : knee;
|
|
703
|
+
crotch = look.style === "athlete" ? shorts : b.knee;
|
|
537
704
|
}
|
|
538
705
|
for (let y = crotch; y < b.hip; y++) {
|
|
539
|
-
put(0, y,
|
|
706
|
+
put(0, y, bottom.main);
|
|
540
707
|
}
|
|
541
708
|
}
|
|
542
|
-
function
|
|
543
|
-
const
|
|
709
|
+
function paintTorsoFront({ upper, b, look, gear }) {
|
|
710
|
+
const top = topOf[look.style];
|
|
544
711
|
const torso = b.shoulder - b.hip + 1;
|
|
545
712
|
const chestRows = Math.ceil(torso * 0.45);
|
|
546
713
|
const halfAt = (y) => {
|
|
@@ -549,175 +716,457 @@ function paintTorso({ upper, b, look, gear }) {
|
|
|
549
716
|
}
|
|
550
717
|
return y - b.hip < 2 ? Math.max(b.hips, b.waist) : b.waist;
|
|
551
718
|
};
|
|
552
|
-
for (let y = b.hip; y
|
|
719
|
+
for (let y = b.hip; y < b.shoulder; y++) {
|
|
553
720
|
const half = halfAt(y);
|
|
554
|
-
span(upper, -half, half, y, main);
|
|
555
|
-
upper(-half, y,
|
|
556
|
-
upper(half, y, shade);
|
|
721
|
+
span(upper, -half, half, y, top.main);
|
|
722
|
+
upper(-half, y, top.light);
|
|
723
|
+
upper(half, y, top.shade);
|
|
557
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.
|
|
558
734
|
span(upper, -1, 1, b.shoulder + 1, "s");
|
|
735
|
+
upper(1, b.shoulder + 1, "q");
|
|
559
736
|
switch (look.style) {
|
|
560
737
|
case "desk": {
|
|
561
|
-
// A hoodie: the hood
|
|
562
|
-
upper
|
|
563
|
-
upper
|
|
564
|
-
upper
|
|
565
|
-
|
|
566
|
-
|
|
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);
|
|
567
751
|
break;
|
|
568
752
|
}
|
|
569
753
|
case "standing": {
|
|
570
754
|
// A collared shirt under an apron that hangs over the thighs.
|
|
571
|
-
upper
|
|
572
|
-
upper(1, b.shoulder, "w");
|
|
755
|
+
span(upper, -2, 2, b.shoulder, "w");
|
|
573
756
|
upper(0, b.shoulder, "s");
|
|
574
757
|
for (const x of [-2, 2]) {
|
|
575
|
-
upper(x, b.shoulder, "S");
|
|
576
758
|
upper(x, b.shoulder - 1, "S");
|
|
759
|
+
upper(x, b.shoulder - 2, "S");
|
|
577
760
|
}
|
|
578
|
-
for (let y = b.hip -
|
|
761
|
+
for (let y = b.hip - 3; y <= b.shoulder - 3; y++) {
|
|
579
762
|
span(upper, -b.waist, b.waist, y, "S");
|
|
763
|
+
upper(b.waist, y, "z");
|
|
764
|
+
upper(-b.waist, y, "T");
|
|
580
765
|
}
|
|
581
|
-
span(upper, -
|
|
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");
|
|
582
770
|
break;
|
|
583
771
|
}
|
|
584
772
|
case "active": {
|
|
585
|
-
|
|
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);
|
|
586
777
|
break;
|
|
587
778
|
}
|
|
588
779
|
case "athlete": {
|
|
589
|
-
|
|
590
|
-
span(upper, -
|
|
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
|
+
}
|
|
591
789
|
break;
|
|
592
790
|
}
|
|
593
791
|
}
|
|
594
792
|
if (gear.wear === 2) {
|
|
595
793
|
// An open jacket over whatever is underneath.
|
|
596
794
|
for (let y = b.hip; y <= b.shoulder; y++) {
|
|
597
|
-
const half = halfAt(y);
|
|
598
|
-
span(upper, -half, -half + 1, y, "
|
|
795
|
+
const half = y === b.shoulder ? b.chest + 1 : halfAt(y);
|
|
796
|
+
span(upper, -half, -half + 1, y, "M");
|
|
599
797
|
span(upper, half - 1, half, y, "X");
|
|
600
798
|
}
|
|
799
|
+
upper(-2, b.shoulder, "M");
|
|
800
|
+
upper(2, b.shoulder, "X");
|
|
601
801
|
}
|
|
602
802
|
if (gear.carry >= 1) {
|
|
603
803
|
// Backpack straps over both shoulders.
|
|
604
|
-
for (let y = b.shoulder -
|
|
804
|
+
for (let y = b.shoulder - 4; y <= b.shoulder; y++) {
|
|
605
805
|
upper(-(b.chest - 1), y, "B");
|
|
606
806
|
upper(b.chest - 1, y, "B");
|
|
607
807
|
}
|
|
808
|
+
upper(-(b.chest - 1), b.shoulder - 3, "d");
|
|
809
|
+
upper(b.chest - 1, b.shoulder - 3, "d");
|
|
608
810
|
}
|
|
609
811
|
if (gear.wear >= 1) {
|
|
610
812
|
// A scarf around the neck, its end hanging down the chest.
|
|
611
813
|
span(upper, -2, 2, b.shoulder + 1, "S");
|
|
612
|
-
upper
|
|
613
|
-
|
|
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
|
+
}
|
|
614
819
|
}
|
|
615
820
|
if (gear.carry === 2) {
|
|
616
821
|
// A gold medal on its ribbon.
|
|
617
|
-
upper(-1, b.shoulder, "z");
|
|
618
|
-
upper(1, b.shoulder, "z");
|
|
619
|
-
upper(0, b.shoulder -
|
|
620
|
-
upper
|
|
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");
|
|
621
827
|
}
|
|
622
828
|
}
|
|
623
|
-
|
|
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") {
|
|
624
831
|
const isJacket = gear.wear === 2;
|
|
625
|
-
const
|
|
832
|
+
const tones = isJacket
|
|
833
|
+
? { main: "X", shade: "X", light: "M" }
|
|
834
|
+
: topOf[look.style];
|
|
626
835
|
const sleeve = isJacket ? 1 : sleeveOf[look.style];
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
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;
|
|
641
862
|
}
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
upper(wrist[0], wrist[1], "t");
|
|
863
|
+
for (let index = 0; index < width; index++) {
|
|
864
|
+
upper(x + dx * index, y + dy * index, index === width - 1 ? edge : key);
|
|
645
865
|
}
|
|
646
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
|
+
}
|
|
647
872
|
}
|
|
648
|
-
function
|
|
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
|
+
};
|
|
649
878
|
const { chin } = b;
|
|
650
|
-
const hair =
|
|
879
|
+
const [hair, light] = hairOf[look.hair];
|
|
651
880
|
const turn = pose.turn ?? 0;
|
|
652
|
-
const jaw = look.build === "heavy" ?
|
|
653
|
-
span(
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
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.
|
|
658
895
|
const eye = pose.eyes === "closed" ? "q" : "e";
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
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");
|
|
662
901
|
if (pose.mouth === "smile") {
|
|
663
|
-
|
|
902
|
+
span(head, turn - 1, turn + 1, chin + 1, "m");
|
|
903
|
+
head(turn - 2, chin + 2, "r");
|
|
904
|
+
head(turn + 2, chin + 2, "r");
|
|
664
905
|
}
|
|
665
906
|
else if (pose.mouth === "open") {
|
|
666
|
-
|
|
907
|
+
span(head, turn - 1, turn, chin + 1, "e");
|
|
667
908
|
}
|
|
668
|
-
|
|
669
|
-
|
|
909
|
+
else {
|
|
910
|
+
span(head, turn - 1, turn, chin + 1, "q");
|
|
911
|
+
}
|
|
912
|
+
// Short hair with sideburns and ears, a side-swept fringe, or a bob.
|
|
670
913
|
if (look.gender === "male") {
|
|
671
|
-
|
|
672
|
-
|
|
914
|
+
head(-3, chin + 4, hair);
|
|
915
|
+
head(3, chin + 4, hair);
|
|
673
916
|
}
|
|
674
917
|
else if (look.gender === "female") {
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
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);
|
|
678
922
|
}
|
|
679
923
|
else {
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
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");
|
|
932
|
+
}
|
|
933
|
+
if (gear.gadget >= 1) {
|
|
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");
|
|
683
941
|
}
|
|
684
942
|
}
|
|
685
|
-
|
|
686
|
-
|
|
943
|
+
}
|
|
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
|
+
}
|
|
687
1040
|
}
|
|
688
1041
|
if (gear.gadget >= 1) {
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
span(upper, -1, 1, chin + 5, "k");
|
|
693
|
-
for (const x of [-3, 3]) {
|
|
694
|
-
upper(x, chin + 1, "k");
|
|
695
|
-
upper(x, chin + 2, "k");
|
|
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");
|
|
696
1045
|
}
|
|
697
1046
|
}
|
|
698
1047
|
if (gear.headwear === 1) {
|
|
699
|
-
span(upper, -
|
|
700
|
-
span(upper, -2,
|
|
701
|
-
span(upper,
|
|
1048
|
+
span(upper, -3, 2, chin + 6, "S");
|
|
1049
|
+
span(upper, -2, 1, chin + 7, "S");
|
|
1050
|
+
span(upper, 2, 5, chin + 5, "z");
|
|
702
1051
|
}
|
|
703
1052
|
else if (gear.headwear === 2) {
|
|
704
|
-
span(upper,
|
|
705
|
-
|
|
706
|
-
|
|
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");
|
|
707
1108
|
}
|
|
708
|
-
upper(0, chin + 5, "t");
|
|
709
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);
|
|
1113
|
+
}
|
|
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");
|
|
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);
|
|
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);
|
|
710
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
|
+
};
|
|
711
1159
|
export function paletteOf(palette) {
|
|
712
1160
|
const { cloak, cloakShade, scarf, scarfShade, glow } = palette.sprite;
|
|
713
1161
|
return new Map([
|
|
714
|
-
["A", "#
|
|
1162
|
+
["A", "#141414"],
|
|
715
1163
|
["B", "#3E3226"],
|
|
716
1164
|
["b", "#6B4A2E"],
|
|
717
1165
|
["C", cloak],
|
|
718
1166
|
["c", cloakShade],
|
|
719
1167
|
["D", "#50555F"],
|
|
720
1168
|
["d", "#9AA0AA"],
|
|
1169
|
+
["E", mix(cloak, "#FFFFFF", 0.16)],
|
|
721
1170
|
["e", "#2B2B2B"],
|
|
722
1171
|
["F", "#C8473F"],
|
|
723
1172
|
["G", "#6FA35A"],
|
|
@@ -725,9 +1174,12 @@ export function paletteOf(palette) {
|
|
|
725
1174
|
["H", "#9C9C9C"],
|
|
726
1175
|
["h", "#4A3222"],
|
|
727
1176
|
["I", "#E4E4E4"],
|
|
728
|
-
["
|
|
1177
|
+
["i", "#6B4A33"],
|
|
1178
|
+
["J", "#BDBDBD"],
|
|
1179
|
+
["K", "#C4C4C4"],
|
|
729
1180
|
["L", "#4A505B"],
|
|
730
1181
|
["l", glow],
|
|
1182
|
+
["M", "#424A58"],
|
|
731
1183
|
["m", "#B5524A"],
|
|
732
1184
|
["N", "#B59A6A"],
|
|
733
1185
|
["n", "#937B52"],
|
|
@@ -737,8 +1189,11 @@ export function paletteOf(palette) {
|
|
|
737
1189
|
["p", "#33565D"],
|
|
738
1190
|
["Q", "#E6E0D4"],
|
|
739
1191
|
["q", "#C99872"],
|
|
1192
|
+
["R", "#4F8189"],
|
|
1193
|
+
["r", "#E8909A"],
|
|
740
1194
|
["S", scarf],
|
|
741
1195
|
["s", "#E2B48C"],
|
|
1196
|
+
["T", mix(scarf, "#FFFFFF", 0.22)],
|
|
742
1197
|
["t", glow],
|
|
743
1198
|
["U", "#B4AE9F"],
|
|
744
1199
|
["V", "#4A5536"],
|
|
@@ -746,18 +1201,18 @@ export function paletteOf(palette) {
|
|
|
746
1201
|
["w", "#F2F2F2"],
|
|
747
1202
|
["X", "#2F3540"],
|
|
748
1203
|
["x", "#3D414B"],
|
|
1204
|
+
["Y", "#C8C8C8"],
|
|
749
1205
|
["y", palette.soft],
|
|
750
1206
|
["Z", "#C8C8C8"],
|
|
751
1207
|
["z", scarfShade],
|
|
752
1208
|
]);
|
|
753
1209
|
}
|
|
754
1210
|
// The pixel keys of 1 frame, row by row, "." where nothing is drawn.
|
|
755
|
-
export function composeScene({ activity, counter, level, look, mood, width, }) {
|
|
1211
|
+
export function composeScene({ activity, counter, level, look, mood, width, anchor = homeOf(width), }) {
|
|
756
1212
|
const canvas = Array.from({ length: stageHeight }, () => Array.from({ length: width }, () => "."));
|
|
757
|
-
const scene = sceneOf(activity, counter, width, mood);
|
|
1213
|
+
const scene = sceneOf(activity, counter, width, mood, anchor);
|
|
758
1214
|
const b = bodyOf(look);
|
|
759
1215
|
const pose = scene.pose(b);
|
|
760
|
-
const lift = pose.lift ?? 0;
|
|
761
1216
|
// The character's x = 0 is its center column, mirrored when facing left.
|
|
762
1217
|
const plant = (rise) => (x, y, key) => {
|
|
763
1218
|
const column = scene.x + scene.facing * x;
|
|
@@ -766,25 +1221,25 @@ export function composeScene({ activity, counter, level, look, mood, width, }) {
|
|
|
766
1221
|
row[column] = key;
|
|
767
1222
|
}
|
|
768
1223
|
};
|
|
769
|
-
const put = plant(lift);
|
|
770
|
-
const
|
|
1224
|
+
const put = plant(scene.lift);
|
|
1225
|
+
const bob = pose.view === "front" ? (pose.bob ?? 0) : 0;
|
|
771
1226
|
const paint = {
|
|
772
1227
|
put,
|
|
773
|
-
upper,
|
|
1228
|
+
upper: plant(scene.lift - bob),
|
|
774
1229
|
b,
|
|
775
1230
|
look,
|
|
776
1231
|
gear: gearOf(level),
|
|
777
|
-
pose,
|
|
778
1232
|
counter,
|
|
779
1233
|
};
|
|
780
|
-
paintShadow(plant(0), b, lift);
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
1234
|
+
paintShadow(plant(0), b, scene.lift);
|
|
1235
|
+
if (pose.view === "side") {
|
|
1236
|
+
paintSide(paint, pose);
|
|
1237
|
+
}
|
|
1238
|
+
else {
|
|
1239
|
+
paintFront(paint, pose);
|
|
1240
|
+
}
|
|
786
1241
|
for (const draw of scene.props ?? []) {
|
|
787
|
-
draw(put, upper, b);
|
|
1242
|
+
draw(put, paint.upper, b);
|
|
788
1243
|
}
|
|
789
1244
|
return canvas.map((row) => row.join(""));
|
|
790
1245
|
}
|